diff --git a/packages/core/src/compiler.ts b/packages/core/src/compiler.ts index e9dcaa4fd..edcd7a449 100644 --- a/packages/core/src/compiler.ts +++ b/packages/core/src/compiler.ts @@ -12,7 +12,9 @@ import { indent } from './indent.js'; import { hasProperty } from './core.js'; -declare var process: any; +declare var process: { + env: Record; +} | undefined; const indentCode = ('undefined' !== typeof process && process.env?.DEBUG || '').includes('deepkit'); diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 0247e7b80..0ed0c7655 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -464,6 +464,10 @@ export function getInheritanceChain(classType: ClassType): ClassType[] { } declare var v8debug: any; +declare var process: { + execArgv: string[]; + platform: string; +} | undefined; export function inDebugMode() { return typeof v8debug === 'object' || @@ -497,7 +501,7 @@ export function getCurrentFileName(offset: number = 0): string { if (path.indexOf('file') >= 0) { path = new URL(path).pathname; } - if (path[0] === '/' && process.platform === 'win32') { + if (path[0] === '/' && 'undefined' !== typeof process && process.platform === 'win32') { path = path.slice(1); } return path; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 7d409c336..1de4e615e 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -19,10 +19,6 @@ "es2020", "es2022.error" ], - "types": [ - "dot-prop", - "node" - ], "skipLibCheck": true }, "reflection": true, @@ -38,4 +34,4 @@ "path": "../bench/tsconfig.json" } ] -} \ No newline at end of file +} diff --git a/packages/http/src/router.ts b/packages/http/src/router.ts index b0e1a8b09..b5b69a4db 100644 --- a/packages/http/src/router.ts +++ b/packages/http/src/router.ts @@ -8,18 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ import { ClassType, CompilerContext, getClassName, isArray, isClass, urlJoin } from '@deepkit/core'; -import { - entity, - ReflectionClass, - ReflectionFunction, - ReflectionKind, - ReflectionParameter, - SerializationOptions, - serializer, - Serializer, - Type, - ValidationError, -} from '@deepkit/type'; +import { entity, ReflectionClass, ReflectionFunction, ReflectionKind, ReflectionParameter, SerializationOptions, serializer, Serializer, Type, ValidationError } from '@deepkit/type'; import { getActions, HttpAction, httpClass, HttpController, HttpDecorator } from './decorator.js'; import { HttpRequest, HttpRequestPositionedParameters, HttpRequestQuery, HttpRequestResolvedParameters } from './model.js'; import { InjectorContext, InjectorModule } from '@deepkit/injector'; @@ -30,7 +19,7 @@ import { HttpMiddlewareConfig, HttpMiddlewareFn } from './middleware.js'; //@ts-ignore import qs from 'qs'; -import { HtmlResponse, JSONResponse, Response } from './http.js'; +import { HtmlResponse, JSONResponse, Redirect, Response } from './http.js'; import { getRequestParserCodeForParameters, ParameterForRequestParser, parseRoutePathToRegex } from './request-parser.js'; import { HttpConfig } from './module.config.js'; @@ -352,7 +341,7 @@ export interface HttpRouterFunctionOptions { 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 }; + return { ...defaultOptions, ...options, methods }; } /** @@ -444,6 +433,13 @@ export abstract class HttpRouterRegistryFunctionRegistrar { this.register(convertOptions(['HEAD'], pathOrOptions, this.defaultOptions), callback); } + public redirectCallback(pathOrOptions: string | HttpRouterFunctionOptions, callback: (request: HttpRequest) => string, redirectCode: number = 302) { + const options = convertOptions(['GET'], pathOrOptions, this.defaultOptions); + this.register(options, (request: HttpRequest) => { + return Redirect.toUrl(callback(request), redirectCode); + }); + } + private register(options: HttpRouterFunctionOptions, callback: (...args: any[]) => any, module?: InjectorModule) { const fn = ReflectionFunction.from(callback); @@ -536,7 +532,7 @@ export class HttpRouterRegistry extends HttpRouterRegistryFunctionRegistrar { type: 'controller', controller, module, - methodName: action.methodName + methodName: action.methodName, }; const routeConfig = createRouteConfigFromHttpAction(routeAction, action, module, controllerData); @@ -578,7 +574,7 @@ export class HttpRouter { controllers: (ClassType | { module: InjectorModule, controller: ClassType })[], middlewareRegistry: MiddlewareRegistry = new MiddlewareRegistry(), module: InjectorModule = new InjectorModule(), - config: HttpConfig = new HttpConfig() + config: HttpConfig = new HttpConfig(), ): HttpRouter { return new this(new HttpControllers(controllers.map(v => { return isClass(v) ? { controller: v, module } : v; @@ -636,7 +632,7 @@ export class HttpRouter { resolverForToken: routeConfig.resolverForToken, resolverForParameterName: routeConfig.resolverForParameterName, pathParameterNames: parsedRoute.pathParameterNames, - routeConfig + routeConfig, }); let methodCheck = ''; diff --git a/packages/type/src/reflection/processor.ts b/packages/type/src/reflection/processor.ts index 1b7d2ec6b..0c6a605fd 100644 --- a/packages/type/src/reflection/processor.ts +++ b/packages/type/src/reflection/processor.ts @@ -688,7 +688,7 @@ export class Processor { case ReflectionOp.parameter: { const ref = this.eatParameter() as number; const t: Type = { kind: ReflectionKind.parameter, parent: undefined as any, name: program.stack[ref] as string, type: this.pop() as Type }; - t.type.parent = t; + if (t.type) t.type.parent = t; this.pushType(t); break; } diff --git a/sync-tsconfig-deps.js b/sync-tsconfig-deps.js index 43c4baa0c..1bfc8a383 100644 --- a/sync-tsconfig-deps.js +++ b/sync-tsconfig-deps.js @@ -45,7 +45,7 @@ for (const name of packages) { } function filter(name) { - return !['api-console-gui', 'framework-debug-gui', 'orm-browser-gui'].includes(name); + return !['api-console-gui', 'framework-debug-gui', 'orm-browser-gui', 'desktop-ui'].includes(name); } for (const [name, config] of Object.entries(packageConfigs)) { diff --git a/tsconfig.json b/tsconfig.json index c3f8deeb4..7d8bc13bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,9 +37,6 @@ { "path": "./packages/create-app/tsconfig.json" }, - { - "path": "./packages/desktop-ui/tsconfig.json" - }, { "path": "./packages/devtool/tsconfig.json" }, @@ -161,4 +158,4 @@ "path": "./packages/workflow/tsconfig.json" } ] -} \ No newline at end of file +} diff --git a/website/README.md b/website/README.md index 5c687ef92..5be8c3dec 100644 --- a/website/README.md +++ b/website/README.md @@ -19,3 +19,11 @@ cd website/ docker build -t website2 -f Dockerfile ../ docker run --rm -p 8080:8080 -e app_databaseHost=host.docker.internal website2 ``` + + +## Translate + +```sh +export APP_OPENAI_API_KEY=your_openai_api_key +npm run translate de +``` diff --git a/website/package.json b/website/package.json index c7d168036..5a3b6bda6 100644 --- a/website/package.json +++ b/website/package.json @@ -10,6 +10,7 @@ "build": "npm run api-docs && BUILD=angular NODE_OPTIONS=--preserve-symlinks ng build --configuration production", "test": "jest" }, + "type": "commonjs", "private": true, "devDependencies": { "@angular-devkit/build-angular": "^20.0.3", diff --git a/website/src/app/app.routes.ts b/website/src/app/app.routes.ts index e8fa69547..0200969e5 100644 --- a/website/src/app/app.routes.ts +++ b/website/src/app/app.routes.ts @@ -1,19 +1,31 @@ -import { ActivatedRouteSnapshot, CanActivate, Route, Routes } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, Route, Router, Routes, UrlMatcher, UrlSegment } from '@angular/router'; import { StartpageComponent } from '@app/app/pages/startpage.component'; import { DocumentationComponent } from '@app/app/pages/documentation.component'; import { EmptyComponent } from '@app/app/pages/empty.component'; -import { CommunityQuestionsComponent } from '@app/app/pages/documentation/community-questions.component'; import { DocumentationPageComponent } from '@app/app/pages/documentation/page.component'; -import { CommunityQuestionComponent } from '@app/app/pages/documentation/community-question.component'; -import { ExamplesComponent } from '@app/app/pages/documentation/examples.component'; -import { ExampleComponent } from '@app/app/pages/documentation/example.component'; -import { DocuSearchComponent } from '@app/app/pages/documentation/search.component'; import { StaticPageComponent } from '@app/app/pages/static-page.component'; import { NotFoundComponent } from '@app/app/pages/not-found.component'; import { Injectable } from '@angular/core'; import { BlogComponent, BlogListComponent, BlogPostDetailComponent } from '@app/app/pages/blog.component'; import { BookComponent } from '@app/app/pages/documentation/book.component.js'; +export const legacyDocMatcher: UrlMatcher = (segments: UrlSegment[]) => { + if (segments.length === 0 || segments[0].path !== 'documentation') return null; + const rest = segments.slice(1).map(s => s.path).join('/'); + return { consumed: segments, posParams: { rest: new UrlSegment(rest, {}) } }; +}; + +@Injectable({ providedIn: 'root' }) +export class LegacyDocRedirectGuard implements CanActivate { + constructor(private router: Router) {} + canActivate(route: ActivatedRouteSnapshot): false { + const rest = (route.params['rest'] as string) || ''; + const target = rest ? `/en/documentation/${rest}` : `/en/documentation`; + this.router.navigateByUrl(target, { replaceUrl: true }); + return false; + } +} + function redirect(from: string, to: string): Route { return { path: from, @@ -36,12 +48,20 @@ export const routes: Routes = [ redirect('documentation/type/serialization', 'documentation/runtime-types/serialization'), redirect('documentation/framework/rpc/controller', 'documentation/rpc/getting-started'), + { path: 'documentation', pathMatch: 'full', redirectTo: 'en/documentation' }, + { matcher: legacyDocMatcher, canActivate: [LegacyDocRedirectGuard], component: EmptyComponent }, + // { // path: 'benchmarks', // loadChildren: () => import('./benchmarks/benchmarks.module').then(m => m.BenchmarksModule), // }, - { path: '', pathMatch: 'full', component: StartpageComponent, data: { floatHeader: true } }, + { path: '', pathMatch: 'full', component: StartpageComponent }, + { path: 'zh', pathMatch: 'full', component: StartpageComponent }, + { path: 'ko', pathMatch: 'full', component: StartpageComponent }, + { path: 'ja', pathMatch: 'full', component: StartpageComponent }, + { path: 'de', pathMatch: 'full', component: StartpageComponent }, + { path: 'admin', loadComponent: () => import('./pages/admin/admin.component').then(v => v.AdminComponent) }, // { path: 'library', component: LibrariesComponent }, // { path: 'library/:id', component: LibraryComponent }, @@ -51,29 +71,29 @@ export const routes: Routes = [ { path: 'about-us', component: StaticPageComponent, data: { page: 'about-us' } }, { path: 'contact', component: StaticPageComponent, data: { page: 'contact' } }, { path: 'data-protection', component: StaticPageComponent, data: { page: 'data-protection' } }, - { path: 'documentation/book', component: BookComponent }, + { path: ':lang/documentation/book', component: BookComponent }, { - path: 'blog', component: BlogComponent, children: [ + path: ':lang/blog', component: BlogComponent, children: [ { path: '', pathMatch: 'full', component: BlogListComponent }, { path: ':slug', component: BlogPostDetailComponent }, ], }, { - path: 'documentation', + path: ':lang/documentation', component: DocumentationComponent, data: { search: true, footer: false, hideLogo: true }, children: [ - { - path: 'questions', component: EmptyComponent, children: [ - { path: 'post/:slug', component: CommunityQuestionComponent }, - { path: '**', component: CommunityQuestionsComponent }, - ], - }, - { path: 'search', component: DocuSearchComponent }, - { path: 'examples', component: ExamplesComponent }, + // { + // path: 'questions', component: EmptyComponent, children: [ + // { path: 'post/:slug', component: CommunityQuestionComponent }, + // { path: '**', component: CommunityQuestionsComponent }, + // ], + // }, + // { path: 'search', component: DocuSearchComponent }, + // { path: 'examples', component: ExamplesComponent }, { path: 'desktop-ui', loadChildren: () => import('./pages/documentation/desktop-ui/desktop-ui.module').then(v => v.DocDesktopUIModule) }, - { path: ':category/examples/:slug', component: ExampleComponent }, - { path: ':category/examples', component: ExamplesComponent }, + // { path: ':category/examples/:slug', component: ExampleComponent }, + // { path: ':category/examples', component: ExamplesComponent }, { path: '**', component: DocumentationPageComponent }, ], }, diff --git a/website/src/app/components/content-render.component.ts b/website/src/app/components/content-render.component.ts index 0ee4e8832..1dab1eb4e 100644 --- a/website/src/app/components/content-render.component.ts +++ b/website/src/app/components/content-render.component.ts @@ -6,7 +6,8 @@ import { Router } from '@angular/router'; import { AppImagesComponent } from '@app/app/components/images.component'; import { ImageComponent } from '@app/app/components/image.component'; import { DomSanitizer } from '@angular/platform-browser'; -import { ContentApiDocsComponent } from '@app/app/components/content-api-docs.component.js'; +import { ContentApiDocsComponent } from '@app/app/components/content-api-docs.component'; +import { Translation } from '@app/app/components/translation'; const whitelist = ['div', 'p', 'a', 'button', 'iframe', 'pre', 'span', 'code', 'strong', 'hr', 'ul', 'li', 'ol', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'table', 'tbody', 'tr', 'td', 'th', 'boxes', 'box']; @@ -164,6 +165,7 @@ export class ContentRenderComponent implements OnInit, OnChanges { private renderer: Renderer2, private router: Router, private injector: EnvironmentInjector, + private translation: Translation, private app: ApplicationRef, ) { } @@ -353,7 +355,8 @@ export class ContentRenderComponent implements OnInit, OnChanges { component.setInput('title', meta.title || ''); component.instance.onRender.subscribe(() => { - this.onRender.emit() + console.log('coder rendered'); + this.onRender.emit(); }); this.app.attachView(component.hostView); return [{ node: component.location.nativeElement }]; @@ -393,6 +396,9 @@ export class ContentRenderComponent implements OnInit, OnChanges { const url = new URL(content.props.href, new URL(this.linkRelativeTo || this.router.url, base)); let href = url.pathname.replace('.md', ''); if (url.hash) href += url.hash; + if (href.startsWith('/documentation')) { + href = this.translation.lang() + href; + } this.renderer.setAttribute(element, 'href', href); } this.hookRouter(element); diff --git a/website/src/app/components/footer.component.css b/website/src/app/components/footer.component.css index 1e84b0e35..b95124727 100644 --- a/website/src/app/components/footer.component.css +++ b/website/src/app/components/footer.component.css @@ -16,7 +16,7 @@ .wrapper.made-in { margin-top: 0; font-size: 13px; - color: var(--color-grey2); + color: var(--color-grey); padding-top: 0px; padding-bottom: 43px; justify-content: center; @@ -40,7 +40,7 @@ } .text { - color: var(--color-grey2); + color: var(--color-grey); margin-top: 20px; } } @@ -51,13 +51,17 @@ nav { flex-direction: column; a, a:link { - color: var(--color-grey2); + color: var(--color-grey); font-size: 12px; letter-spacing: 1px; line-height: 16px; text-align: right; text-decoration: none; } + + a.active { + color: var(--color-link); + } } nav.social { diff --git a/website/src/app/components/footer.component.ts b/website/src/app/components/footer.component.ts index 9050bd767..b79cafd9a 100644 --- a/website/src/app/components/footer.component.ts +++ b/website/src/app/components/footer.component.ts @@ -1,32 +1,43 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { RouterLink } from '@angular/router'; +import { Translation } from '@app/app/components/translation'; @Component({ selector: 'dw-footer', template: ` -
- - -
-
- Made in Germany +
+ + +
+
+ Made in Germany +
`, imports: [ - RouterLink + RouterLink, ], styleUrls: ['./footer.component.css'] }) export class FooterComponent { + translation = inject(Translation); + get year() { return new Date().getFullYear(); } diff --git a/website/src/app/components/header.component.ts b/website/src/app/components/header.component.ts index c58b716c2..41ce7793a 100644 --- a/website/src/app/components/header.component.ts +++ b/website/src/app/components/header.component.ts @@ -1,13 +1,16 @@ -import { Component, input, signal } from '@angular/core'; +import { Component, inject, input, signal } from '@angular/core'; import { Router, RouterLink, RouterLinkActive } from '@angular/router'; - import { ReactiveFormsModule } from '@angular/forms'; import { SearchComponent } from '@app/app/components/search.component'; +import { DropdownComponent, OpenDropdownDirective } from '@deepkit/desktop-ui'; +import { TranslatePipe, Translation } from '@app/app/components/translation'; +import { texts } from '@app/common/docs'; @Component({ selector: 'dw-header-logo', template: ` - + @let url = translation.lang(); + `, styles: ` :host { @@ -24,17 +27,33 @@ import { SearchComponent } from '@app/app/components/search.component'; ], }) export class HeaderLogoComponent { + translation = inject(Translation); } @Component({ selector: 'dw-header-nav', template: ` +
+ @let translationRoutes = translation.routes(); + @let currentLanguage = translation.lang(); + + + @for (lang of translation.languages; track $index) { + {{ lang.label }} + } + + + @let labels = translation.labels(); + {{ labels[currentLanguage] }} +
- - Docs - Blog + + {{ texts.docs|translate }} + {{ texts.blog|translate }} `, imports: [ RouterLinkActive, RouterLink, + DropdownComponent, + OpenDropdownDirective, + TranslatePipe, ], styles: ` :host { + font-size: 14px; display: flex; align-items: center; justify-content: flex-end; + flex-wrap: wrap; flex: 1; - gap: 30px; - height: 80px; + gap: 1px 30px; + text-align: left; } .socials { @@ -74,12 +98,36 @@ export class HeaderLogoComponent { a, a:link { opacity: 0.7; + &:hover { opacity: 1; } } } + .language a, .language a:link { + cursor: pointer; + } + + ::ng-deep .languages-dropdown { + border: 0; + background: black !important; + box-shadow: unset !important; + border: unset !important; + text-align: left; + } + + ::ng-deep .languages-dropdown > div { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 14px; + } + + ::ng-deep .languages-dropdown a, .languages-dropdown a:link { + padding: 0 12px; + } + svg, img { width: 15px; height: 15px; @@ -87,8 +135,6 @@ export class HeaderLogoComponent { a, a:link { text-decoration: none; - text-align: right; - font-size: 14px; color: #E3ECF0; font-weight: 600; line-height: 26px; @@ -113,7 +159,8 @@ export class HeaderLogoComponent { `, }) export class HeaderNavComponent { - + translation = inject(Translation); + protected readonly texts = texts; } @Component({ @@ -128,17 +175,17 @@ export class HeaderNavComponent { } - - - - - - - - - - - + + + + + + + + + + +
`, diff --git a/website/src/app/components/highlight-code.component.ts b/website/src/app/components/highlight-code.component.ts index e02ba7b9e..943cfb298 100644 --- a/website/src/app/components/highlight-code.component.ts +++ b/website/src/app/components/highlight-code.component.ts @@ -83,13 +83,13 @@ export class HighlightCodeComponent { if (!code) return ''; try { - const html = this.prism.highlight(code, this.lang()); - setTimeout(() => this.onRender.emit()); - return html; + return this.prism.highlight(code, this.lang()); } catch (error) { console.error('Error highlighting code:', error); // Fallback to plain text if highlighting fails return `${code}`; + } finally { + this.onRender.emit(); } })); } diff --git a/website/src/app/components/more-languages.component.ts b/website/src/app/components/more-languages.component.ts new file mode 100644 index 000000000..5bf780abf --- /dev/null +++ b/website/src/app/components/more-languages.component.ts @@ -0,0 +1,45 @@ +import { Component, inject } from '@angular/core'; +import { Translation } from '@app/app/components/translation'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-more-languages', + template: ` + @let translationRoutes = translation.routes(); + @let currentLanguage = translation.lang(); + + @for (lang of translation.languages; track $index) { + {{ lang.label }} + } + `, + styles: ` + :host { + display: flex; + padding: 10px 0; + flex-direction: row; + justify-content: flex-end; + flex-wrap: wrap; + gap: 12px; + } + + a, a:link { + color: var(--color-grey); + font-size: 12px; + letter-spacing: 1px; + line-height: 16px; + text-align: right; + text-decoration: none; + } + + a:hover, a.active { + color: var(--color-link); + } + `, + imports: [ + RouterLink, + ], +}) +export class MoreLanguagesComponent { + translation = inject(Translation); +} diff --git a/website/src/app/components/table-of-content.component.ts b/website/src/app/components/table-of-content.component.ts index d60084778..aa1d909db 100644 --- a/website/src/app/components/table-of-content.component.ts +++ b/website/src/app/components/table-of-content.component.ts @@ -9,6 +9,25 @@ type HeaderInfo = { label: string, indent: number, top: number, height: number, export class TableOfContentService { headers = signal([]); content = signal(undefined); + contentUpdated = createNotifier(); + + constructor() { + effect(() => { + this.content(); + this.contentUpdated.listen(); + this.updateHeaders(); + }); + } + + protected lastFrame: ReturnType | undefined; + + triggerUpdate() { + if (this.lastFrame) return; + this.lastFrame = setTimeout(() => { + this.lastFrame = undefined; + this.contentUpdated.notify(); + }, 10); + } unregister(content: HTMLElement) { if (this.content() !== content) return; @@ -16,12 +35,19 @@ export class TableOfContentService { this.headers.set([]); } - render(content: HTMLElement) { + render(content?: HTMLElement) { if ('undefined' === typeof window) return; - this.content.set(content); + } + + protected updateHeaders() { + const content = this.content(); + if (!content || !content.getBoundingClientRect) { + this.headers.set([]); + return; + } + const headers: HeaderInfo[] = []; - const viewportHeight = window.innerHeight; const rect = content.getBoundingClientRect(); const headerElements = content.querySelectorAll('h1, h2, h3, h4'); @@ -33,12 +59,12 @@ export class TableOfContentService { headers.push({ label, indent, top, link, height: 0 }); } - // if nothing overflows, we don't need a table of content - const lastHeader = headers[headers.length - 1]; - if (headers.length === 0 || lastHeader.top < viewportHeight) { - this.headers.set([]); - return; - } + // // if nothing overflows, we don't need a table of content + // const lastHeader = headers[headers.length - 1]; + // if (headers.length === 0 || lastHeader.top < viewportHeight) { + // this.headers.set([]); + // return; + // } for (let i = 0; i < headers.length; i++) { const header = headers[i]; @@ -59,20 +85,18 @@ export class TableOfContentService { selector: 'app-table-of-content', template: `
- @if (enabled()) { - @for (line of lines(); track $index) { -
- } + @for (line of lines(); track $index) { +
+ } - @let tops = this.headersTop(); - @for (header of toc.headers(); track $index) { - @let top = tops[$index]; - @if (top >= 0) { - {{ header.label }} - } + @let tops = this.headersTop(); + @for (header of toc.headers(); track $index) { + @let top = tops[$index]; + @if (top >= 0) { + {{ header.label }} } }
@@ -90,8 +114,8 @@ export class TableOfContentService { transition: 0.1s ease-out; &.active { - background-color: #fff; - height: 2px; + background-color: rgba(255, 255, 255, 0.8); + /*height: 2px;*/ } } @@ -137,6 +161,11 @@ export class TableOfContentService { backdrop-filter: blur(7px); border-radius: 12px; transition: 0.1s ease-out; + opacity: 0; + + &.enabled { + opacity: 1; + } &.enabled:hover { min-width: 230px; @@ -165,8 +194,7 @@ export class TableOfContentComponent { elementRef = viewChild.required('wrapper', { read: ElementRef }); resize = createNotifier(); - activeHeader = signal(undefined); - activeLine = signal(undefined); + viewport = signal({ top: 0, bottom: 0 }); enabled = computed(() => this.contentTextService.tocVisible() && this.toc.headers().length > 0); @@ -192,32 +220,23 @@ export class TableOfContentComponent { this.updateActiveHeader(); } + isActiveLine(y: number) { + const section = this.viewport(); + const containerHeight = this.containerHeight(); + y /= containerHeight; // Normalize to 0-1 range + return y >= section.top && y <= section.bottom; + } + private updateActiveHeader() { + this.toc.contentUpdated.listen(); const content = this.toc.content(); if (!content || !content.getBoundingClientRect) return; - const scrollTop = window.scrollY; - const rect = content.getBoundingClientRect(); - const contentHeight = rect.height; - const offsetTop = rect.top + scrollTop; - - const containerHeight = this.containerHeight(); - const scrollPercentage = (scrollTop - offsetTop) / contentHeight; - const y = scrollPercentage * containerHeight; - - const index = Math.round(y / this.interval); - this.activeLine.set(index >= 0 ? index : undefined); - const headers = this.toc.headers(); - - for (let i = headers.length - 1; i >= 0; i--) { - const header = headers[i]; - const absTop = offsetTop + header.top * contentHeight; - if (scrollTop + this.interval >= absTop) { - this.activeHeader.set(header.link); - return; - } - } - - this.activeHeader.set(undefined); + const contentRect = content.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + this.viewport.set({ + top: -contentRect.top / contentRect.height, + bottom: (-contentRect.top + viewportHeight) / contentRect.height, + }); } lines = computed(() => { @@ -239,7 +258,7 @@ export class TableOfContentComponent { let previousTop = -1; for (let i = 0; i < headers.length; i++) { const header = headers[i]; - const stickyTop = this.protected(header.top); + const stickyTop = this.headerTop(header.top); if (stickyTop === previousTop) continue; // Skip if the top is the same as the previous one headersTop[i] = stickyTop; previousTop = stickyTop; @@ -251,11 +270,10 @@ export class TableOfContentComponent { /** * We get a number like 0.945 and want to make it sticky to the interval of 20px. */ - protected (percentage: number): number { + protected headerTop(percentage: number): number { const containerHeight = this.containerHeight(); const top = percentage * containerHeight; - const stickyTop = Math.round(top / this.interval) * this.interval; - return stickyTop / containerHeight; // Return as percentage + return Math.round(top / this.interval) * this.interval; } } diff --git a/website/src/app/components/translation.ts b/website/src/app/components/translation.ts new file mode 100644 index 000000000..43fc9594e --- /dev/null +++ b/website/src/app/components/translation.ts @@ -0,0 +1,110 @@ +import { computed, inject, Injectable, Pipe, PipeTransform, Signal, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { ControllerClient } from '@app/app/client'; +import { derivedAsync } from 'ngxtension/derived-async'; +import { pendingTask } from '@deepkit/desktop-ui'; + + +@Injectable({ providedIn: 'root' }) +export class Translation { + + languages = [ + { code: 'en', label: 'English' }, + { code: 'zh', label: '中文 (Chinese)' }, + { code: 'ko', label: '한국어 (Korean)' }, + { code: 'ja', label: '日本語 (Japanese)' }, + { code: 'de', label: 'Deutsch (German)' }, + ]; + + routes = computed(() => { + const routes: Record = {}; + const labels = this.labels(); + let path = this.url(); + + const firstSlash = path.indexOf('/', 1); + if (firstSlash !== -1) { + const lang = path.substring(1, firstSlash === -1 ? path.length : firstSlash); + if (labels[lang]) { + path = path.substring(firstSlash); + } + } else { + path = ''; + } + + for (const lang of this.languages) { + routes[lang.code] = `/${lang.code}${path}`; + } + if (routes['en'] === '/en') routes['en'] = '/'; + return routes; + }); + + labels = computed(() => { + const labels: Record = {}; + for (const lang of this.languages) { + labels[lang.code] = lang.label; + } + return labels; + }); + + router = inject(Router); + + url = signal(this.router.url); + + lang = computed(() => { + const lang = this.url().replace(/^\//, '').split('/')[0] || 'en'; + return this.labels()[lang] ? lang : 'en'; + }); + + client = inject(ControllerClient); + + basics = this.translations('basics'); + + constructor() { + this.router.events.subscribe(() => { + this.url.set(this.router.url); + }); + } + + async getTranslation(name: string): Promise> { + return await this.client.main.getTranslation(this.lang(), name); + } + + translations(name: string): Signal> { + return derivedAsync(pendingTask(async () => { + const t = await this.getTranslation(name); + return t; + }), { + initialValue: {}, + }); + } + + translate(key: string): string { + const translations = this.basics(); + // console.log(key, translations); + return translations[key] || key; + } +} + +@Pipe({ name: 'translate', pure: false }) +export class TranslatePipe implements PipeTransform { + translation = inject(Translation); + + transform(key: string): string { + return this.translation.translate(key); + } +} + + +@Pipe({ name: 'i18nRoute' }) +export class i18nRoutePipe implements PipeTransform { + translation = inject(Translation); + + transform(route: string[] | string): string[] { + const lang = this.translation.lang(); + if (typeof route === 'string') { + route = [route]; + } + if (lang === 'en') return route; + return [lang, ...route]; + } +} diff --git a/website/src/app/pages/blog.component.css b/website/src/app/pages/blog.component.css index eb1f4f053..a50326f2c 100644 --- a/website/src/app/pages/blog.component.css +++ b/website/src/app/pages/blog.component.css @@ -1,7 +1,6 @@ :host { display: flex; flex-direction: row; - min-height: calc(100vh - 35px); } .logo-container { @@ -15,10 +14,6 @@ } } -.header { - /*background: #090A0B;*/ -} - .sidebar { flex: 0 0 calc(50% - 280px); position: relative; @@ -77,7 +72,9 @@ } .header { - flex: 0; + flex: 0 0 80px; + display: flex; + align-items: center; } } diff --git a/website/src/app/pages/documentation.component.css b/website/src/app/pages/documentation.component.css index 917fddfec..8ab26b2d0 100644 --- a/website/src/app/pages/documentation.component.css +++ b/website/src/app/pages/documentation.component.css @@ -165,11 +165,15 @@ nav { } } +.page-header { + min-height: 80px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + .menu-trigger { - position: fixed; - z-index: 100; - top: 25px; - left: 24px; display: none; @media (max-width: 860px) { @@ -194,7 +198,6 @@ nav { border-radius: 15px; background: var(--color-content-text-bg); padding-top: 30px; - padding-bottom: 50vh; /*background: linear-gradient(90deg, #13161C 0%, #0F1217 10.59%);*/ } diff --git a/website/src/app/pages/documentation.component.ts b/website/src/app/pages/documentation.component.ts index 38f167368..b94095fdc 100644 --- a/website/src/app/pages/documentation.component.ts +++ b/website/src/app/pages/documentation.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, effect, ElementRef, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, inject, OnDestroy, ViewChild } from '@angular/core'; import { IsActiveMatchOptions, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { Subscription } from 'rxjs'; @@ -7,7 +7,8 @@ import { HeaderLogoComponent, HeaderNavComponent } from '@app/app/components/hea import { TableOfContentComponent } from '@app/app/components/table-of-content.component.js'; import { ContentTextService } from '@app/app/components/content-text.component.js'; import { DuiApp } from '@deepkit/desktop-ui'; -import { docs } from '@app/common/docs'; +import { docs, texts } from '@app/common/docs'; +import { TranslatePipe, Translation } from '@app/app/components/translation.js'; @Component({ imports: [ @@ -18,6 +19,7 @@ import { docs } from '@app/common/docs'; HeaderNavComponent, HeaderLogoComponent, TableOfContentComponent, + TranslatePipe, ], standalone: true, styleUrls: ['./documentation.component.css'], @@ -30,19 +32,23 @@ import { docs } from '@app/common/docs';
@for (doc of docs; track $index) {
-
{{ doc.category }}
+ @if (doc.category) { +
{{ doc.category|translate }}
+ } @for (page of doc.pages; track $index) { {{ page.title }} + routerLink="/{{translation.lang()}}/documentation/{{ page.path }}">{{ page.title|translate }} }
}
- +
-
@@ -67,6 +73,7 @@ export class DocumentationComponent implements AfterViewInit, OnDestroy { @ViewChild('nav') nav?: ElementRef; sub: Subscription; + translation = inject(Translation); constructor( public platform: PlatformHelper, @@ -105,4 +112,6 @@ export class DocumentationComponent implements AfterViewInit, OnDestroy { ngAfterViewInit() { setTimeout(() => this.scrollToActiveLink(), 100); } + + protected readonly texts = texts; } diff --git a/website/src/app/pages/documentation/desktop-ui/api-doc.component.ts b/website/src/app/pages/documentation/desktop-ui/api-doc.component.ts index 6adba74f5..03ee05217 100644 --- a/website/src/app/pages/documentation/desktop-ui/api-doc.component.ts +++ b/website/src/app/pages/documentation/desktop-ui/api-doc.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, computed, ContentChildren, inject, Injectable, input, Input, QueryList, signal } from '@angular/core'; +import { AfterViewInit, Component, computed, ContentChildren, forwardRef, inject, Injectable, input, Input, QueryList, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CodeHighlightComponent } from '@deepkit/ui-library'; import { ButtonGroupComponent, InputComponent, pendingTask, TabButtonComponent, TableCellDirective, TableColumnDirective, TableComponent } from '@deepkit/desktop-ui'; @@ -471,7 +471,7 @@ interface TableRow { InputComponent, FormsModule, TableCellDirective, - ContentRenderComponent, + forwardRef(() => ContentRenderComponent), CodeHighlightComponent, ], }) diff --git a/website/src/app/pages/documentation/desktop-ui/app.component.ts b/website/src/app/pages/documentation/desktop-ui/app.component.ts index ce0d0c70c..7ce8f10d4 100644 --- a/website/src/app/pages/documentation/desktop-ui/app.component.ts +++ b/website/src/app/pages/documentation/desktop-ui/app.component.ts @@ -1,13 +1,12 @@ import { Component } from '@angular/core'; -import { ApiDocComponent } from '@app/app/pages/documentation/desktop-ui/api-doc.component.js'; -import { AppTitle } from '@app/app/components/title.js'; +import { ApiDocComponent } from '@app/app/pages/documentation/desktop-ui/api-doc.component'; +import { AppTitle } from '@app/app/components/title'; @Component({ host: { ngSkipHydration: 'true' }, imports: [ ApiDocComponent, AppTitle, - ], template: `
diff --git a/website/src/app/pages/documentation/page.component.ts b/website/src/app/pages/documentation/page.component.ts index 8c6f5cc3f..73772895b 100644 --- a/website/src/app/pages/documentation/page.component.ts +++ b/website/src/app/pages/documentation/page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ElementRef, inject, OnDestroy, OnInit, signal, viewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, ElementRef, inject, OnDestroy, OnInit, signal, viewChild } from '@angular/core'; import { bodyToString, Content, Page, parseBody, projectMap } from '@app/common/models'; import { AppDescription, AppTitle } from '@app/app/components/title'; import { ContentRenderComponent } from '@app/app/components/content-render.component'; @@ -10,6 +10,8 @@ import { PageResponse } from '@app/app/page-response'; import { waitForInit } from '@app/app/utils'; import { ContentTextComponent } from '@app/app/components/content-text.component.js'; import { TableOfContentService } from '@app/app/components/table-of-content.component.js'; +import { MoreLanguagesComponent } from '@app/app/components/more-languages.component.js'; +import { Translation } from '@app/app/components/translation.js'; @Component({ imports: [ @@ -18,6 +20,7 @@ import { TableOfContentService } from '@app/app/components/table-of-content.comp ContentRenderComponent, LoadingComponent, ContentTextComponent, + MoreLanguagesComponent, ], styleUrls: ['./page.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -58,6 +61,7 @@ import { TableOfContentService } from '@app/app/components/table-of-content.comp } +
`, }) @@ -69,10 +73,12 @@ export class DocumentationPageComponent implements OnInit, OnDestroy { project = signal(''); subline = signal(undefined); currentPath = signal(''); + currentLang = signal(''); content = viewChild('content', { read: ElementRef }); toc = inject(TableOfContentService); + translation = inject(Translation); constructor( private pageResponse: PageResponse, @@ -82,6 +88,13 @@ export class DocumentationPageComponent implements OnInit, OnDestroy { public router: Router, ) { waitForInit(this, 'load'); + effect(() => { + this.toc.render(this.content()?.nativeElement); + }); + effect(() => { + this.translation.lang(); + void this.load(this.currentPath()); + }); } ngOnInit() { @@ -109,14 +122,16 @@ export class DocumentationPageComponent implements OnInit, OnDestroy { path = path || 'index'; if (project) path = project + '/' + path; - if (this.currentPath() === path) return; + const lang = this.translation.lang(); + if (this.currentPath() === path && this.currentLang() === lang) return; this.error.set(''); this.loading.set(true); this.currentPath.set(path); + this.currentLang.set(lang); try { - const page = await this.client.main.getPage('documentation/' + path); + const page = await this.client.main.getPage('documentation/' + path, lang); if (!page) return; this.page.set(page); this.subline.set(parseBody(page.body).subline); @@ -141,10 +156,6 @@ export class DocumentationPageComponent implements OnInit, OnDestroy { } loadTableOfContent() { - const content = this.content(); - if (!content || !content.nativeElement) return; - setTimeout(() => { - this.toc.render(content.nativeElement); - }, 100); + this.toc.triggerUpdate(); } } diff --git a/website/src/app/pages/libraries/libraries.component.ts b/website/src/app/pages/libraries/libraries.component.ts index 32d7e62d7..26a2ec227 100644 --- a/website/src/app/pages/libraries/libraries.component.ts +++ b/website/src/app/pages/libraries/libraries.component.ts @@ -1,9 +1,10 @@ -import { Component, input } from '@angular/core'; +import { Component, inject, input } from '@angular/core'; import { RouterLink } from '@angular/router'; import { AppTitle } from '@app/app/components/title'; import { HeaderComponent } from '@app/app/components/header.component.js'; import { FooterComponent } from '@app/app/components/footer.component.js'; -import { libraries } from '@app/common/docs.js'; +import { libraries, texts } from '@app/common/docs.js'; +import { i18nRoutePipe, TranslatePipe, Translation } from '@app/app/components/translation.js'; @Component({ selector: 'a[package]', @@ -55,7 +56,7 @@ import { libraries } from '@app/common/docs.js'; font-size: 12px; white-space: nowrap; } - ` + `, }) export class LibraryEntryComponent { title = input.required(); @@ -104,7 +105,7 @@ export class LibraryEntryComponent { .twice { grid-column: 1 / span 2; } - + @media (max-width: 640px) { .main, .twice { grid-column: unset; @@ -154,22 +155,24 @@ export class LibraryEntryComponent { LibraryEntryComponent, RouterLink, FooterComponent, + TranslatePipe, + i18nRoutePipe, ], template: `
@@ -177,15 +180,19 @@ export class LibraryEntryComponent {
{{ category.category }}
@for (library of category.items; track $index) { - + }
}
+
- - ` + + `, }) export class LibrariesComponent { protected readonly libraries = libraries; + protected readonly texts = texts; + + translation = inject(Translation); } diff --git a/website/src/app/pages/libraries/library.component.ts b/website/src/app/pages/libraries/library.component.ts index 571f78f8b..26df18992 100644 --- a/website/src/app/pages/libraries/library.component.ts +++ b/website/src/app/pages/libraries/library.component.ts @@ -125,7 +125,7 @@ export class LibraryComponent implements OnInit { async load(slug: string) { try { - const page = await this.client.main.getPage('library/' + slug); + const page = await this.client.main.getPage('library/' + slug, 'en'); this.page.set(page); this.subline.set(parseBody(page.body).subline); diff --git a/website/src/app/pages/static-page.component.ts b/website/src/app/pages/static-page.component.ts index cc1a0490a..2918a3cd9 100644 --- a/website/src/app/pages/static-page.component.ts +++ b/website/src/app/pages/static-page.component.ts @@ -54,7 +54,7 @@ export class StaticPageComponent { page = derivedAsync(pendingTask(async () => { const slug = this.routeData().page; if (!slug) return undefined; - return this.client.main.getPage('static/' + slug); + return this.client.main.getPage('static/' + slug, 'en'); })); subline = computed(() => { diff --git a/website/src/common/docs.ts b/website/src/common/docs.ts index 652853e50..84d80396d 100644 --- a/website/src/common/docs.ts +++ b/website/src/common/docs.ts @@ -4,6 +4,16 @@ export type DocCategory = { pages: { path: string; title: string, book?: boolean }[]; } +export const texts = { + banner1: 'Deepkit is a modular framework for TypeScript backend web applications.', + banner2: 'Structured, scalable, and built for enterprise-grade architecture.', + gettingStarted: 'Getting Started', + viewOnGitHub: 'View on GitHub', + docs: 'Docs', + blog: 'Blog', + chapters: 'Chapters', +} as const; + export const docs: DocCategory[] = [ { category: null, diff --git a/website/src/pages/documentation/app.md b/website/src/pages/documentation/app.md index 809b17fbe..8fac49e18 100644 --- a/website/src/pages/documentation/app.md +++ b/website/src/pages/documentation/app.md @@ -168,7 +168,7 @@ As soon as you import Deepkit Framework you get additional providers. See [Deepk ## Exit code -The exit code is 0 by default, which means that the command was executed successfully. To change the exit code, a number other than 0 should be returned in the exucute method. +The exit code is 0 by default, which means that the command was executed successfully. To change the exit code, a number other than 0 should be returned in the execute method or command callback. ```typescript diff --git a/website/src/pages/documentation/dependency-injection/configuration.md b/website/src/pages/documentation/dependency-injection/configuration.md index fab726b67..1ae867552 100644 --- a/website/src/pages/documentation/dependency-injection/configuration.md +++ b/website/src/pages/documentation/dependency-injection/configuration.md @@ -43,7 +43,7 @@ class RootConfiguration { ## Validation -Also, all serialization and validation types from the previous chapters [Validation](validation.md) and [Serialization](serialization.md) can be used to specify in great detail what type and content restrictions an option must have. +Also, all serialization and validation types from the previous chapters [Validation](../runtime-types/validation.md) and [Serialization](../runtime-types/serialization.md) can be used to specify in great detail what type and content restrictions an option must have. ```typescript class RootConfiguration { diff --git a/website/src/pages/documentation/index.md b/website/src/pages/documentation/index.md index 76adca271..cc2364b7b 100644 --- a/website/src/pages/documentation/index.md +++ b/website/src/pages/documentation/index.md @@ -18,7 +18,7 @@ on [GitHub](https://github.com/deepkit/deepkit-framework). - [Broker](/documentation/broker.md) - Message broker abstraction to work with distributed L2 cache, pub/sub, queues, central atomic locks, or key-value store. - [HTTP](/documentation/http.md) - HTTP server abstraction to build type-safe endpoints. - [RPC](/documentation/rpc.md) - Remote procedure call abstraction to connect frontend with backend, or to connect multiple backend services. -- [ORM](/documentation/orm.md) - Use Deepkit's database abstraction to store and query data in a type-safe way. +- [ORM](/documentation/orm.md) - ORM and DBAL to store and query data in a type-safe way. - [Desktop-UI](/documentation/desktop-ui/getting-started) - Build GUI applications with Deepkit's Angular-based UI framework. ## API Reference diff --git a/website/src/pages/documentation/rpc/dependency-injection.md b/website/src/pages/documentation/rpc/dependency-injection.md index 117b963ce..0b565b90c 100644 --- a/website/src/pages/documentation/rpc/dependency-injection.md +++ b/website/src/pages/documentation/rpc/dependency-injection.md @@ -52,4 +52,4 @@ const kernel = new RpcKernel(injector); kernel.registerController(Controller); ``` -See [Dependency Injection](xref:dependency-injection.adoc) to learn more. +See [Dependency Injection](../dependency-injection.md) to learn more. diff --git a/website/src/pages/documentation/runtime-types/serialization.md b/website/src/pages/documentation/runtime-types/serialization.md index 72a3ed9c9..8689e9029 100644 --- a/website/src/pages/documentation/runtime-types/serialization.md +++ b/website/src/pages/documentation/runtime-types/serialization.md @@ -152,18 +152,3 @@ const result = deserialize(data, {loosely: false}); ``` In the case of invalid data, no attempt is made to convert it and instead an error message is thrown. - -## Type Annotations - -### Integer - -### Group - -### Excluded - -### Mapped - -### Embedded - -## Naming Strategy - diff --git a/website/src/server/app.ts b/website/src/server/app.ts index 59c50c15a..85d666ea5 100644 --- a/website/src/server/app.ts +++ b/website/src/server/app.ts @@ -25,6 +25,7 @@ import { AdminFilesController } from '@app/server/controller/admin-files.control import { Filesystem } from '@deepkit/filesystem'; import { FilesystemDatabaseAdapter } from '@deepkit/filesystem-database'; import { translateCommand } from '@app/server/commands/translate'; +import { httpWorkflow } from '@deepkit/http'; (global as any).window = undefined; (global as any).document = undefined; @@ -94,6 +95,18 @@ app.setup((module, config) => { serveFilesystem(module, { baseUrl: 'media/' }); }); +app.listen(httpWorkflow.onRequest, (event) => { + const url = event.request.url || ''; + if (url.startsWith('/documentation') || url.startsWith('/blog')) { + // move all /documentation/* to /en/documentation/* + const to = `/en${url}`; + event.response.setHeader('Location', to); + event.response.status(302); + event.response.end(`Redirecting to ${to}`); + event.send(event.response); + } +}); + app.command('search:index', async (search: Search) => await search.index()); app.command('search:find', async (query: string, search: Search) => { console.log(await search.find(query)); diff --git a/website/src/server/commands/import.ts b/website/src/server/commands/import.ts index e9de2a934..4ba64b44d 100644 --- a/website/src/server/commands/import.ts +++ b/website/src/server/commands/import.ts @@ -5,13 +5,13 @@ import { PageProcessor } from '@app/server/page-processor'; import { CommunityMessage } from '@app/common/models'; import { getCurrentDirName } from '@deepkit/core'; -const dirname = getCurrentDirName(); +const currentDir = getCurrentDirName(); export async function importExamples( database: Database, page: PageProcessor, ) { - const dir = findParentPath('src/pages/examples', dirname); + const dir = findParentPath('src/pages/examples', currentDir); if (!dir) throw new Error('Examples folder not found'); const files = await readdir(dir); @@ -41,7 +41,7 @@ export async function importQuestions( database: Database, page: PageProcessor, ) { - const dir = findParentPath('src/pages/questions', dirname); + const dir = findParentPath('src/pages/questions', currentDir); if (!dir) throw new Error('Examples folder not found'); const files = await readdir(dir); diff --git a/website/src/server/commands/translate.ts b/website/src/server/commands/translate.ts index d07697e73..e4fabd3c2 100644 --- a/website/src/server/commands/translate.ts +++ b/website/src/server/commands/translate.ts @@ -1,44 +1,93 @@ import { OpenAI } from 'openai'; import { Logger } from '@deepkit/logger'; -import { docs, libraries } from '@app/common/docs'; -import { join } from 'path'; +import path from 'path'; +import { mkdirSync, readFileSync, writeFileSync } from 'fs'; import { getCurrentDirName } from '@deepkit/core'; +import { docs, libraries, texts } from '@app/common/docs'; -const dirname = getCurrentDirName(); +const currentDir = getCurrentDirName(); -export async function translateCommand( - lang: string = 'cn', - logger: Logger, - openAi: OpenAI, -) { - const supportedLanguages = ['cn', 'kr', 'de', 'fr', 'pl']; +const languages: Record = { + zh: '中文 (Chinese)', + ko: '한국어 (Korean)', + ja: '日本語 (Japanese)', + de: 'Deutsch (German)', + pl: 'Polski (Polish)', +}; - if (!supportedLanguages.includes(lang)) { - throw new Error(`Unsupported language: ${lang}`); - } +const instruction = ` +Words like Error, Class, Function, Variable, Type, Interface, Method, Property, Parameter, Argument, Return Value, refer to TypeScript technical terms. +Keep error message strings in code in English, do not translate them. +You can translate code comments if needed, but do not translate any code itself. +Make sure to keep technical terms in English if possible. If it would sound weird or uncommon in the target language even in technical documentations, keep it in English. +`; - logger.info(`Starting translation to ${lang}...`); +async function translateLabels(openAi: OpenAI, lang: string, labels: string[]): Promise> { + const completion = await openAi.chat.completions.create({ + model: 'gpt-5', messages: [ + { + role: 'system', + content: ` +You are a helpful assistant that translates technical documentation and code comments into ${lang} ${languages[lang]}. +Please ensure the translation is accurate and maintains the original meaning. +You get from the user either just markdown or TypeScript code. Keep the structure and formatting of the original text +as much as possible, and do not add any additional explanations or comments. +Do not translate any code, only the comments and markdown. +If you encounter any text that is not in English, do not translate it. - const files = [ - join(dirname, '../../common/docs.ts'), - ]; +Return a map of translations for the provided labels. +Example input: ["Hello World", "This is a test"] +Example output: { + "Hello World": "你好,世界", + "This is a test": "这是一个测试" +} +Make sure to use the language: ${lang} ${languages[lang]} and return the translations in a JSON format only (no markdown). +`.trim(), + }, + { + role: 'user', + content: `Translate the following text into ${lang} ${languages[lang]}: ${JSON.stringify(labels)}`, + }, + ], + }); + if (completion.choices.length === 0) { + throw new Error('No translation received from OpenAI'); + } + let translatedText = completion.choices[0].message.content; + if (!translatedText) return {}; + + if (translatedText.startsWith('```json')) translatedText.slice(7, -3); + try { + return JSON.parse(translatedText); + } catch (error) { + throw new Error(`Failed to parse translation response: ${translatedText}`); + } +} + +async function translateMarkdown(openAi: OpenAI, lang: string, source: string): Promise { const completion = await openAi.chat.completions.create({ - model: 'gpt-4o', messages: [ + model: 'gpt-5', messages: [ { role: 'system', content: ` -You are a helpful assistant that translates technical documentation and code comments into ${lang}. +You are a helpful assistant that translates technical documentation and code comments into ${lang} ${languages[lang]}. Please ensure the translation is accurate and maintains the original meaning. You get from the user either just markdown or TypeScript code. Keep the structure and formatting of the original text as much as possible, and do not add any additional explanations or comments. Do not translate any code, only the comments and markdown. If you encounter any text that is not in English, do not translate it. + +Return markdown in the same format as the input, but translated to ${lang} ${languages[lang]}. +Do not return any additional text or explanations. +${instruction} + +Make sure to also translate link titles, headings, and any other text that is not code. `.trim(), }, { role: 'user', - content: `Translate the following text into ${lang}:\n\n${docs.join('\n')}\n\n${libraries.join('\n')}`, + content: `Translate the following text into ${lang} ${languages[lang]}: ${source}`, }, ], }); @@ -46,7 +95,134 @@ If you encounter any text that is not in English, do not translate it. if (completion.choices.length === 0) { throw new Error('No translation received from OpenAI'); } - const translatedText = completion.choices[0].message.content; - logger.info(`Translation to ${lang} completed.`); - console.log(translatedText); + let translatedText = completion.choices[0].message.content; + return translatedText || ''; +} + +export function readTranslationsFile(lang: string, name: string): Record { + const filePath = path.join(currentDir, '../../translations', lang, `${name}.json`); + try { + const content = readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } catch (error) { + return {}; + } +} + +export function readTranslationDocumentationPage(lang: string, page: string): string { + const filePath = path.join(currentDir, '../../translations/documentation', lang, page); + try { + return readFileSync(filePath, 'utf8'); + } catch (error) { + return ''; + } +} + +function writeTranslationsFile(lang: string, name: string, translations: Record) { + const filePath = path.join(currentDir, '../../translations', lang, `${name}.json`); + writeFileSync(filePath, JSON.stringify(translations, null, 2), 'utf8'); // Ensure the file exists +} + +function filterAlreadyTranslated(translations: Record, labels: string[]): string[] { + return labels.filter(label => !translations[label]); +} + +// navigation, startpage, etc. All in one big mapping file that is loaded at once. +async function translateBasics(openAi: OpenAI, lang: string) { + const labels: string[] = []; + + for (const [key, label] of Object.entries(texts)) { + labels.push(label); + } + + for (const category of docs) { + if (category.category) labels.push(category.category); + if (category.category === 'API') continue; + for (const page of category.pages) { + labels.push(page.title); + } + } + + for (const category of libraries) { + if (category.category) labels.push(category.category); + for (const item of category.items) { + labels.push(item.title); + labels.push(item.description); + } + } + + const translations = readTranslationsFile(lang, 'basics'); + const filtered = filterAlreadyTranslated(translations, labels); + const newTranslations = await translateLabels(openAi, lang, filtered); + Object.assign(translations, newTranslations); + writeTranslationsFile(lang, 'basics', translations); +} + +type Hash = string; +type Path = string; +type TranslationState = Record; + +function readState(lang: string): TranslationState { + const dir = path.join(currentDir, '../../translations', lang); + const filePath = path.join(dir, 'state.json'); + console.log('readState', filePath); + mkdirSync(dir, { recursive: true }); + try { + const content = readFileSync(filePath, 'utf8'); + return JSON.parse(content) || {}; + } catch (error) { + return {}; + } +} + +function writeState(lang: string, state: TranslationState) { + const filePath = path.join(currentDir, '../../translations', lang, 'state.json'); + writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf8'); +} + +async function translateDocumentationPage(openAi: OpenAI, state: TranslationState, lang: string, page: string) { + console.log(`Translating ${page} to ${lang}...`); + const filePath = path.join(currentDir, '../../pages/documentation', page); + const binary = readFileSync(filePath); + const hash = Buffer.from(await crypto.subtle.digest('SHA-256', binary)).toString('hex'); + if (state[page] === hash) { + console.log(`Skipping ${page}, already translated.`); + return; + } + + const content = binary.toString('utf8'); + const translatedContent = await translateMarkdown(openAi, lang, content); + const outputPath = path.join(currentDir, '../../translations', lang, 'documentation', page); + mkdirSync(path.dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, translatedContent, 'utf8'); + state[page] = hash; + writeState(lang, state); + console.log(`Translated ${page} to ${lang}`); + console.log(translatedContent); +} + +export async function translateCommand( + lang: string = 'cn', + logger: Logger, + openAi: OpenAI, +) { + if (!languages[lang]) { + throw new Error(`Unsupported language: ${lang}`); + } + + logger.info(`Starting translation to ${lang}...`); + const dir = path.join(currentDir, '../../translations', lang); + mkdirSync(dir, { recursive: true }); + + await translateBasics(openAi, lang); + const state = readState(lang); + + for (const category of docs) { + for (const page of category.pages) { + const pagePath = (page.path || 'index') + '.md'; + if (pagePath.startsWith('desktop-ui/')) continue; + await translateDocumentationPage(openAi, state, lang, pagePath); + } + } + // await translateDocumentationPage(openAi, state, lang, 'introduction.md'); } diff --git a/website/src/server/controller/main.controller.ts b/website/src/server/controller/main.controller.ts index 3daf0aa32..2528080ef 100644 --- a/website/src/server/controller/main.controller.ts +++ b/website/src/server/controller/main.controller.ts @@ -11,6 +11,7 @@ import { PageProcessor } from '@app/server/page-processor'; import { Database } from '@deepkit/orm'; import { UserAuthentication } from '@app/server/user-authentication'; import { docs } from '@app/common/docs'; +import { readTranslationsFile } from '@app/server/commands/translate'; function different(a?: string | Content, b?: string | Content): boolean { @@ -231,7 +232,6 @@ export class MainController { @rpc.action() async prompt(url: string, prompt: string): Promise { - const page = await this.getPage(url); return ''; } @@ -257,8 +257,8 @@ export class MainController { } @rpc.action() - async getPage(url: string): Promise { - return await this.page.parse(url); + async getPage(url: string, lang: string): Promise { + return await this.page.parse(url, lang); } @rpc.action() @@ -310,4 +310,12 @@ export class MainController { .filter({ published: true, slug }) .findOneOrUndefined(); } + + @rpc.action() + async getTranslation(lang: string, name: string): Promise> { + // make sure its ascii only, no special characters, a-Z + lang = lang.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase(); + name = name.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase(); + return readTranslationsFile(lang, name); + } } diff --git a/website/src/server/page-processor.ts b/website/src/server/page-processor.ts index 54730e0fc..8e76721e2 100644 --- a/website/src/server/page-processor.ts +++ b/website/src/server/page-processor.ts @@ -1,26 +1,32 @@ -import { findParentPath } from '@deepkit/app'; import { readFile } from 'fs/promises'; import { join } from 'path'; import { magicSeparator, Page } from '@app/common/models'; import { MarkdownParser } from '@app/common/markdown'; -import { dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { getCurrentDirName } from '@deepkit/core'; + +const currentDir = getCurrentDirName(); export class PageProcessor { constructor(protected parser: MarkdownParser) { } - async read(url: string): Promise { - const dir = findParentPath('src/pages'); - if (!dir) throw new Error('Pages folder not found'); + async read(url: string, lang: string = 'en'): Promise { url = url.replace(/[^a-zA-Z0-9\-_\/]/g, ''); const file = url + '.md'; - return await readFile(join(dir, file), 'utf8'); + const originalPath = join(currentDir, '../pages', file); + const translated = join(currentDir, '../translations', lang, file); + console.log(`Reading page from ${originalPath} or ${translated} for language ${lang}`); + if (lang === 'en') return await readFile(originalPath, 'utf8'); + try { + return await readFile(translated, 'utf8'); + } catch { + return await readFile(originalPath, 'utf8'); + } } - async parse(url: string): Promise { + async parse(url: string, lang: string): Promise { await this.parser.load(); - const content = await this.read(url); + const content = await this.read(url, lang); const page = this.parser.parse(content); page.url = url; return page; @@ -37,7 +43,7 @@ export class PageProcessor { const question = text.substr(userStart, assistantStart - userStart).trim(); if (!question) continue; const answer = text.substr(assistantStart + '\nassistant:'.length).trim(); - questions.push({title: question, content: answer}); + questions.push({ title: question, content: answer }); if (questions.length >= top) break; } return questions; @@ -52,7 +58,7 @@ export class PageProcessor { const texts = content.split(magicSeparator); let i = 0; for (const text of texts) { - i++ + i++; if (text.trim() === '') continue; const props: { [name: string]: string } = {}; let lastPropIndex = 0; @@ -75,7 +81,7 @@ export class PageProcessor { props[name] = value; } const content = text.slice(lastPropIndex); - result.push({props, content}); + result.push({ props, content }); if (result.length >= top) break; } @@ -93,7 +99,7 @@ export class PageProcessor { return { title: v.props.title, url: v.props.url, - content: withContent ? v.content : '' + content: withContent ? v.content : '', }; }); } catch (error: any) { diff --git a/website/src/server/search.ts b/website/src/server/search.ts index e8a25a84f..d6fb7047b 100644 --- a/website/src/server/search.ts +++ b/website/src/server/search.ts @@ -115,7 +115,7 @@ ORDER BY rank DESC; for (const file of files) { if (!file.startsWith('documentation/')) continue; - const page = await this.page.parse(file.replace('.md', '')); + const page = await this.page.parse(file.replace('.md', ''), 'en'); // console.log(json); page.url = file.replace('.md', ''); // const body = bodyToString(page.body); diff --git a/website/src/styles.css b/website/src/styles.css index 82954fda1..6faddbebe 100644 --- a/website/src/styles.css +++ b/website/src/styles.css @@ -506,7 +506,6 @@ pre.console { .app-content, .app-content-full { position: relative; - min-height: calc(100vh - 35px); text-align: left; font-size: 16px; line-height: 160%; @@ -560,7 +559,8 @@ html body .normalize-text { ol, ul { text-align: left; - padding-left: 14px; + padding-left: 17px; + margin-left: 0; } li, li p.text { diff --git a/website/src/translations/de/basics.json b/website/src/translations/de/basics.json new file mode 100644 index 000000000..d34863656 --- /dev/null +++ b/website/src/translations/de/basics.json @@ -0,0 +1,160 @@ +{ + "Deepkit is a modular framework for TypeScript backend web applications.": "Deepkit ist ein modulares Framework für TypeScript-Backend-Webanwendungen.", + "Structured, scalable, and built for enterprise-grade architecture.": "Strukturiert, skalierbar und für Enterprise-Architekturen entwickelt.", + "Getting Started": "Erste Schritte", + "View on GitHub": "Auf GitHub ansehen", + "Docs": "Dokumentation", + "Blog": "Blog", + "Chapters": "Kapitel", + "Overview": "Übersicht", + "Introduction": "Einführung", + "App": "App", + "Getting started": "Erste Schritte", + "Arguments & Flags": "Argumente & Flags", + "Dependency Injection": "Dependency Injection", + "Modules": "Module", + "Services": "Services", + "Events": "Event-System", + "Logger": "Logger", + "Configuration": "Konfiguration", + "Framework": "Framework", + "Database": "Datenbank", + "Testing": "Tests", + "Deployment": "Deployment", + "Public Assets": "Öffentliche Assets", + "Runtime Types": "Runtime-Typen", + "Type Annotations": "Typannotationen", + "Reflection": "Reflection", + "Serialization": "Serialisierung", + "Validation": "Validierung", + "Extend": "Erweitern", + "Custom serializer": "Benutzerdefinierter Serializer", + "External Types": "Externe Typen", + "Bytecode": "Bytecode", + "Providers": "Provider", + "Injection": "Injektion", + "Scopes": "Scopes", + "Filesystem": "Dateisystem", + "Local": "Lokal", + "Memory": "Memory", + "AWS S3": "AWS S3", + "FTP": "FTP", + "sFTP (SSH)": "sFTP (SSH)", + "Google Storage": "Google Storage", + "Broker": "Broker", + "Cache": "Cache", + "Message Bus": "Message-Bus", + "Message Queue": "Nachrichtenwarteschlange", + "Atomic Locks": "Atomare Sperren", + "Key Value": "Key-Value", + "HTTP": "HTTP", + "Input & Output": "Eingabe & Ausgabe", + "Views": "Views", + "Middleware": "Middleware", + "Security": "Security", + "RPC": "RPC", + "Errors": "Errors", + "Transport": "Transport", + "Database ORM": "Database ORM", + "Entity": "Entity", + "Session": "Session", + "Query": "Query", + "Transaction": "Transaction", + "Inheritance": "Vererbung", + "Relations": "Beziehungen", + "Migrations": "Migrationen", + "ORM Browser": "ORM-Browser", + "Raw Access": "Rohzugriff", + "Seeding": "Seeding", + "Composite primary key": "Zusammengesetzter Primärschlüssel", + "Soft-Delete": "Soft-Delete", + "Desktop UI": "Desktop-UI", + "Styles": "Stile", + "Adaptive Container": "Adaptiver Container", + "Button": "Button", + "Button group": "Button-Gruppe", + "Dialog": "Dialog", + "Drag": "Drag", + "Dropdown": "Dropdown", + "Icons": "Icons", + "Input": "Input", + "Menu": "Menu", + "Slider": "Slider", + "Radio": "Radio", + "Select": "Select", + "Checkbox": "Checkbox", + "List": "Liste", + "Table": "Table", + "Tabs": "Tabs", + "Window": "Window", + "Window Toolbar": "Window Toolbar", + "API": "API", + "Composition": "Komposition", + "Command line interface (CLI) parser, config loader, dependency injection container, event system, modules system.": "Parser für die Befehlszeilenschnittstelle (CLI), Konfigurationslader, Dependency-Injection-Container, Ereignissystem, Modulsystem.", + "App module that provides application/HTTP/RPC server, worker, debugger, integration tests.": "App-Modul, das Anwendungs-/HTTP-/RPC-Server, Worker, Debugger und Integrationstests bereitstellt.", + "App module that provides HTTP server based on Node http module with validation and serialization.": "App-Modul, das einen HTTP-Server basierend auf dem Node-http-Modul mit Validierung und Serialisierung bereitstellt.", + "Angular SSR": "Angular SSR", + "App module to integrate Angular SSR.": "App-Modul zur Integration von Angular SSR.", + "Infrastructure": "Infrastruktur", + "Remote procedure call (RPC) with binary encoding for WebSockets and TCP.": "Remote Procedure Call (RPC) mit binärer Kodierung für WebSockets und TCP.", + "RPC TCP": "RPC TCP", + "TCP server and client for Deepkit RPC.": "TCP-Server und -Client für Deepkit RPC.", + "Message broker with queues, pub/sub, key-value, 2 level cache, and distributed locks.": "Message-Broker mit Warteschlangen, Pub/Sub, Key-Value, zweistufigem Cache und verteilten Sperren.", + "Broker Redis": "Broker Redis", + "Broker Redis adapter.": "Broker-Redis-Adapter.", + "Unified API to work with local and remote filesystems.": "Einheitliche API für die Arbeit mit lokalen und entfernten Dateisystemen.", + "Filesystem FTP": "Dateisystem FTP", + "Fileystem FTP adapter.": "Dateisystem-FTP-Adapter.", + "Filesystem SFTP": "Dateisystem SFTP", + "Fileystem SFTP (SSH) adapter.": "Dateisystem-SFTP-(SSH)-Adapter.", + "Filesystem S3": "Dateisystem S3", + "Fileystem S3 adapter.": "Dateisystem-S3-Adapter.", + "Filesystem Google": "Dateisystem Google", + "Fileystem Google Storage adapter.": "Dateisystem-Google-Storage-Adapter.", + "Filesystem Database": "Dateisystem Datenbank", + "Fileystem adapter for Deepkit ORM.": "Dateisystem-Adapter für Deepkit ORM.", + "ORM/DBAL": "ORM/DBAL", + "Object-relational Mapper (ORM) and data access library (DAL). MongoDB, SQLite, Postgres, MySQL.": "Objekt-relationaler Mapper (ORM) und Datenzugriffsbibliothek (DAL). MongoDB, SQLite, Postgres, MySQL.", + "ORM MySQL": "ORM MySQL", + "MySQL Adapter for Deepkit ORM.": "MySQL-Adapter für Deepkit ORM.", + "ORM Postgres": "ORM Postgres", + "PostgreSQL Adapter for Deepkit ORM.": "PostgreSQL-Adapter für Deepkit ORM.", + "ORM SQLite": "ORM SQLite", + "SQLite Adapter for Deepkit ORM.": "SQLite-Adapter für Deepkit ORM.", + "ORM Mongo": "ORM Mongo", + "MongoDB Adapter for Deepkit ORM.": "MongoDB-Adapter für Deepkit ORM.", + "Fundamentals": "Grundlagen", + "Type": "Typ", + "Runtime types with reflection, JSON serialization, validation, and type guards.": "Laufzeittypen mit Reflexion, JSON-Serialisierung, Validierung und Type Guards.", + "Event": "Ereignis", + "Async and synchronous event dispatcher.": "Asynchroner und synchroner Ereignis-Dispatcher.", + "Dependency injection (DI) container with modules, config, scopes, and nominal type alias/interface support.": "Dependency-Injection-(DI)-Container mit Modulen, Konfiguration, Geltungsbereichen sowie Unterstützung für nominale Typ-Aliase/Interfaces.", + "Template": "Vorlage", + "HTML template engine based on JSX.": "HTML-Vorlagen-Engine auf Basis von JSX.", + "Logger with scopes, colors, and custom transporter and formatter.": "Logger mit Geltungsbereichen, Farben sowie benutzerdefiniertem Transporter und Formatierer.", + "Workflow": "Workflow", + "Workflow engine / finite state machine.": "Workflow-Engine / Endlicher Automat.", + "Stopwatch": "Stoppuhr", + "Profile and collect execution time of code.": "Profilieren und Ausführungszeiten von Code erfassen.", + "Tools": "Werkzeuge", + "Devtool": "Devtool", + "Chrome devtools for Deepkit (RPC).": "Chrome-DevTools für Deepkit (RPC).", + "Angular Desktop UI library.": "Angular-Desktop-UI-Bibliothek.", + "Web user interface to manage ORM data.": "Weboberfläche zur Verwaltung von ORM-Daten.", + "API console": "API-Konsole", + "Bench": "Bench", + "Tools to run benchmarks and collect statistics.": "Werkzeuge, um Benchmarks auszuführen und Statistiken zu sammeln.", + "Core": "Core", + "BSON": "BSON", + "BSON encoder and decoder.": "BSON-Encoder und -Decoder.", + "Core functions for working with JavaScript.": "Kernfunktionen für die Arbeit mit JavaScript.", + "Topsort": "Topsort", + "Topological sorting algorithm.": "Algorithmus für topologische Sortierung.", + "Runtime": "Laufzeit", + "Vite": "Vite", + "Vite plugin to use Deepkit runtime types.": "Vite-Plugin zur Verwendung der Deepkit-Laufzeittypen.", + "Bun": "Bun", + "Bun plugin to use Deepkit runtime types.": "Bun-Plugin zur Verwendung der Deepkit-Laufzeittypen.", + "Type Compiler": "Typ-Compiler", + "Type compiler as TypeScript plugin to make runtime types available.": "Typ-Compiler als TypeScript-Plugin, um Laufzeittypen verfügbar zu machen." +} diff --git a/website/src/translations/de/documentation/app.md b/website/src/translations/de/documentation/app.md new file mode 100644 index 000000000..136b37ada --- /dev/null +++ b/website/src/translations/de/documentation/app.md @@ -0,0 +1,179 @@ +# Deepkit App + +Die Abstraktion Deepkit App ist der grundlegendste Baustein einer Deepkit-Anwendung. Hier beginnen Sie in der Regel mit dem Aufbau Ihrer Anwendung, wenn Sie die Bibliotheken nicht eigenständig verwenden. +Es handelt sich um eine normale TypeScript-Datei, die Sie mit einer Laufzeitumgebung wie Node.js ausführen. Sie ist der Einstiegspunkt für Ihre Anwendung und bietet eine Möglichkeit, CLI-Befehle, Services, +Konfiguration, Event-System und mehr zu definieren. + +Command-line Interface (CLI)-Programme sind Programme, die über das Terminal in Form von Texteingabe und Textausgabe interagieren. Der Vorteil der Interaktion mit der Anwendung in +dieser Variante besteht darin, dass lediglich ein Terminal entweder lokal oder über eine SSH-Verbindung vorhanden sein muss. + +Sie bietet: + +- CLI-Befehle +- Modulsystem +- Service-Container +- Dependency Injection +- Ereignissystem +- Logger +- Konfigurationslader (env, dotenv, json) + +Ein Befehl in Deepkit hat vollen Zugriff auf den DI-Container und kann dadurch auf alle Provider und Konfigurationsoptionen zugreifen. Die Argumente und Optionen der CLI-Befehle werden über die +Parameterdeklaration mittels TypeScript-Typen gesteuert und automatisch serialisiert und validiert. + +[Deepkit Framework](./framework.md) mit `@deepkit/framework` erweitert dies um einen Anwendungsserver für HTTP/RPC, einen Debugger/Profiler und vieles mehr. + +## Einfache Installation + +Am einfachsten ist der Einstieg mit NPM init, um ein neues Deepkit-Projekt zu erstellen. + +```shell +npm init @deepkit/app@latest my-deepkit-app +```` + +Dadurch wird ein neuer Ordner `my-deepkit-app` mit allen Abhängigkeiten und einer grundlegenden `app.ts`-Datei erstellt. + +```sh +cd my-deepkit-app +npm run app +```` + +Dies führt die Datei `app.ts` mit `ts-node` aus und zeigt Ihnen die verfügbaren Befehle an. Von hier aus können Sie beginnen und eigene Befehle, Controller und so weiter hinzufügen. + +## Manuelle Installation + +Deepkit App basiert auf [Deepkit Runtime Types](./runtime-types.md), daher installieren wir alle Abhängigkeiten: + +```bash +mkdir my-project && cd my-project + +npm install typescript ts-node +npm install @deepkit/app @deepkit/type @deepkit/type-compiler +``` + +Als Nächstes stellen wir sicher, dass Deepkits Type-Compiler in das installierte TypeScript-Paket unter `node_modules/typescript` installiert wird, indem wir den folgenden Befehl ausführen: + +```sh +./node_modules/.bin/deepkit-type-install +``` + +Stellen Sie sicher, dass alle Peer-Abhängigkeiten installiert sind. Standardmäßig installiert NPM 7+ diese automatisch. + +Zum Kompilieren Ihrer Anwendung benötigen wir den TypeScript-Compiler und empfehlen `ts-node`, um die App einfach auszuführen. + +Eine Alternative zur Verwendung von `ts-node` besteht darin, den Quellcode mit dem TypeScript-Compiler zu kompilieren und den JavaScript-Quellcode direkt auszuführen. Dies hat den Vorteil, die Ausführungsgeschwindigkeit für kurze Befehle drastisch zu erhöhen. Es erzeugt jedoch zusätzlichen Workflow-Overhead, entweder durch manuelles Ausführen des Compilers oder das Einrichten eines Watchers. Aus diesem Grund wird in allen Beispielen in dieser Dokumentation `ts-node` verwendet. + +## Erste Anwendung + +Da das Deepkit-Framework keine Konfigurationsdateien oder eine spezielle Ordnerstruktur verwendet, können Sie Ihr Projekt beliebig strukturieren. Die einzigen beiden Dateien, die Sie zum +Start benötigen, sind die TypeScript-Datei app.ts und die TypeScript-Konfiguration tsconfig.json. + +Unser Ziel ist es, die folgenden Dateien in unserem Projektordner zu haben: + +``` +. +├── app.ts +├── node_modules +├── package-lock.json +└── tsconfig.json +``` + +Wir richten eine einfache tsconfig-Datei ein und aktivieren Deepkits Type-Compiler, indem wir `reflection` auf `true` setzen. +Dies ist erforderlich, um den Dependency-Injection-Container und andere Funktionen zu nutzen. + +```json title=tsconfig.json +{ + "compilerOptions": { + "outDir": "./dist", + "experimentalDecorators": true, + "strict": true, + "esModuleInterop": true, + "target": "es2020", + "module": "CommonJS", + "moduleResolution": "node" + }, + "reflection": true, + "files": [ + "app.ts" + ] +} +``` + +```typescript title=app.ts +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +const app = new App(); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +In diesem Code sehen Sie, dass wir einen Testbefehl definiert und eine neue App erstellt haben, die wir direkt mit `run()` ausführen. Durch Ausführen dieses Skripts starten wir die App. + +und führen sie dann direkt aus. + +```sh +$ ./node_modules/.bin/ts-node app.ts +VERSION + Node + +USAGE + $ ts-node app.ts [COMMAND] + +TOPICS + debug + migration Executes pending migration files. Use migration:pending to see which are pending. + server Starts the HTTP server + +COMMANDS + test +``` + +Um nun unseren Testbefehl auszuführen, führen wir den folgenden Befehl aus. + +```sh +$ ./node_modules/.bin/ts-node app.ts test +Hello World +``` + +In Deepkit läuft nun alles über diese `app.ts`. Sie können die Datei nach Belieben umbenennen oder weitere erstellen. Benutzerdefinierte CLI-Befehle, HTTP/RPC-Server, Migrationsbefehle und so weiter werden alle +von diesem Einstiegspunkt aus gestartet. + +## Argumente & Flags + +Deepkit App konvertiert Funktionsparameter automatisch in CLI-Argumente und Flags. Die Reihenfolge der Parameter bestimmt die Reihenfolge der CLI-Argumente + +Parameter können beliebige TypeScript-Typen sein und werden automatisch validiert und deserialisiert. + +Siehe das Kapitel [Argumente & Flags](./app/arguments.md) für weitere Informationen. + +## Dependency Injection + +Deepkit App richtet einen Service-Container ein und für jedes importierte Modul einen eigenen Dependency-Injection-Container, der von seinen Eltern erbt. +Es stellt standardmäßig die folgenden Provider bereit, die Sie automatisch in Ihre Services, Controller und Event-Listener injizieren können: + +- `Logger` für Logging +- `EventDispatcher` für Ereignisbehandlung +- `CliControllerRegistry` für registrierte CLI-Befehle +- `MiddlewareRegistry` für registrierte Middleware +- `InjectorContext` für den aktuellen Injector-Kontext + +Sobald Sie Deepkit Framework importieren, erhalten Sie zusätzliche Provider. Siehe [Deepkit Framework](./framework.md) für weitere Details. + +## Exit-Code + +Der Exit-Code ist standardmäßig 0, was bedeutet, dass der Befehl erfolgreich ausgeführt wurde. Um den Exit-Code zu ändern, sollte eine Zahl ungleich 0 in der Execute-Methode oder dem Befehls-Callback zurückgegeben werden. + +```typescript + +@cli.controller('test') +export class TestCommand { + async execute() { + console.error('Error :('); + return 12; + } +} +``` diff --git a/website/src/translations/de/documentation/app/arguments.md b/website/src/translations/de/documentation/app/arguments.md new file mode 100644 index 000000000..def60ff98 --- /dev/null +++ b/website/src/translations/de/documentation/app/arguments.md @@ -0,0 +1,318 @@ +# Argumente & Flags + +Befehlsargumente im Terminal Ihres Befehls sind einfach normale Argumente der `execute`-Methode oder der Funktion. Sie werden automatisch den Kommandozeilenargumenten zugeordnet. +Wenn Sie einen Parameter als optional markieren, muss er nicht übergeben werden. Wenn Sie einen Standardwert haben, muss er ebenfalls nicht übergeben werden. + +Je nach Typ (string, number, union, etc.) wird der übergebene Wert automatisch deserialisiert und validiert. + +```typescript +import { cli } from '@deepkit/app'; + +//funktional +new App().command('test', (name: string) => { + console.log('Hello', name); +}); + +//Klasse +@cli.controller('test') +class TestCommand { + async execute(name: string) { + console.log('Hello', name); + } +} +``` + +Wenn Sie diesen Befehl jetzt ohne Angabe des Parameters name ausführen, wird ein Fehler ausgegeben: + +```sh +$ ts-node app.ts test +RequiredArgsError: Missing 1 required arg: +name +``` + +Mit `--help` erhalten Sie mehr Informationen über die erforderlichen Argumente: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node-script app.ts test NAME +``` + +Sobald der Name als Argument übergeben wird, wird der Befehl ausgeführt und der Name korrekt übergeben. + +```sh +$ ts-node app.ts test "beautiful world" +Hello beautiful world +``` + +Jeder primitive Parametertyp wie string, number, boolean, String-Literale, deren Union sowie Arrays davon werden automatisch als CLI-Argumente verwendet +und automatisch validiert und deserialisiert. Die Reihenfolge der Parameter bestimmt die Reihenfolge der CLI-Argumente. Sie können so viele Parameter hinzufügen, wie Sie möchten. + +Sobald ein komplexes Objekt (Interface, Klasse, Objektliteral) definiert ist, wird es als Service-Abhängigkeit behandelt +und der Dependency Injection Container versucht, es aufzulösen. Siehe das Kapitel [Abhängigkeitsinjektion](dependency-injection.md) für mehr Informationen. + +## Flags + +Flags sind eine weitere Möglichkeit, Werte an Ihren Befehl zu übergeben. Meistens sind sie optional, müssen es aber nicht sein. Mit dem `Flag`-Typ markierte Parameter können via `--name value` oder `--name=value` übergeben werden. + +```typescript +import { Flag } from '@deepkit/app'; + +//funktional +new App().command('test', (id: number & Flag) => { + console.log('id', name); +}); + +//Klasse +class TestCommand { + async execute(id: number & Flag) { + console.log('id', id); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + --id=id (required) +``` + +In der Hilfe sehen Sie unter „OPTIONS“, dass ein `--id`-Flag erforderlich ist. Wenn Sie dieses Flag korrekt angeben, erhält der Befehl diesen Wert. + +```sh +$ ts-node app.ts test --id 23 +id 23 + +$ ts-node app.ts test --id=23 +id 23 +``` + +### Boolesche Flags + +Flags haben den Vorteil, dass sie auch ohne Wert verwendet werden können, z. B. um ein bestimmtes Verhalten zu aktivieren. Sobald ein Parameter als optionaler Boolean markiert ist, wird dieses Verhalten aktiviert. + +```typescript +import { Flag } from '@deepkit/app'; + +//funktional +new App().command('test', (remove: boolean & Flag = false) => { + console.log('delete?', remove); +}); + +//Klasse +class TestCommand { + async execute(remove: boolean & Flag = false) { + console.log('delete?', remove); + } +} +``` + +```sh +$ ts-node app.ts test +delete? false + +$ ts-node app.ts test --remove +delete? true +``` + +### Mehrfach-Flags + +Um mehrere Werte an dasselbe Flag zu übergeben, kann ein Flag als Array markiert werden. + +```typescript +import { Flag } from '@deepkit/app'; + +//funktional +new App().command('test', (id: number[] & Flag = []) => { + console.log('ids', id); +}); + +//Klasse +class TestCommand { + async execute(id: number[] & Flag = []) { + console.log('ids', id); + } +} +``` + +```sh +$ ts-node app.ts test +ids: [] + +$ ts-node app.ts test --id 12 +ids: [12] + +$ ts-node app.ts test --id 12 --id 23 +ids: [12, 23] +``` + +### Einzelzeichen-Flags + +Um ein Flag auch als einzelnes Zeichen zuzulassen, kann `Flag<{char: 'x'}>` verwendet werden. + +```typescript +import { Flag } from '@deepkit/app'; + +//funktional +new App().command('test', (output: string & Flag<{char: 'o'}>) => { + console.log('output: ', output); +}); + +//Klasse +class TestCommand { + async execute(output: string & Flag<{char: 'o'}>) { + console.log('output: ', output); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + -o, --output=output (required) + + +$ ts-node app.ts test --output test.txt +output: test.txt + +$ ts-node app.ts test -o test.txt +output: test.txt +``` + +## Optional / Standardwert + +Die Signatur der Methode/Funktion definiert, welche Argumente oder Flags optional sind. Ist der Parameter im Typsystem optional, muss der Benutzer ihn nicht angeben. + +```typescript + +//funktional +new App().command('test', (name?: string) => { + console.log('Hello', name || 'nobody'); +}); + +//Klasse +class TestCommand { + async execute(name?: string) { + console.log('Hello', name || 'nobody'); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +Dasselbe gilt für Parameter mit einem Standardwert: + +```typescript +//funktional +new App().command('test', (name: string = 'body') => { + console.log('Hello', name); +}); + +//Klasse +class TestCommand { + async execute(name: string = 'body') { + console.log('Hello', name); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +Dies gilt in gleicher Weise auch für Flags. + + +## Serialisierung / Validierung + +Alle Argumente und Flags werden basierend auf ihren Typen automatisch deserialisiert, validiert und können mit zusätzlichen Einschränkungen versehen werden. + +So ist z. B. bei als number definierten Argumenten im Controller stets garantiert, dass es sich um echte Zahlen handelt, obwohl die Kommandozeile auf Text bzw. Strings basiert. + +```typescript +//funktional +new App().command('test', (id: number) => { + console.log('id', id, typeof id); +}); + +//Klasse +class TestCommand { + async execute(id: number) { + console.log('id', id, typeof id); + } +} +``` + +```sh +$ ts-node app.ts test 123 +id 123 number +``` + +Zusätzliche Einschränkungen können mit den Typannotationen aus `@deepkit/type` definiert werden. + +```typescript +import { Positive } from '@deepkit/type'; +//funktional +new App().command('test', (id: number & Positive) => { + console.log('id', id, typeof id); +}); + +//Klasse +class TestCommand { + async execute(id: number & Positive) { + console.log('id', id, typeof id); + } +} +``` + +Der Typ `Postive` in `id` gibt an, dass nur positive Zahlen erlaubt sind. Übergibt der Benutzer nun eine negative Zahl, wird der Code überhaupt nicht ausgeführt und eine Fehlermeldung angezeigt. + +```sh +$ ts-node app.ts test -123 +Validation error in id: Number needs to be positive [positive] +``` + +Diese zusätzliche, sehr einfach einzurichtende Validierung macht den Befehl deutlich robuster gegenüber falschen Eingaben. Weitere Informationen finden Sie im Kapitel [Validierung](../runtime-types/validation.md). + +## Beschreibung + +Um ein Flag oder Argument zu beschreiben, verwenden Sie den `@description`-Kommentar-Decorator. + +```typescript +import { Positive } from '@deepkit/type'; + +class TestCommand { + async execute( + /** @description Die Benutzerkennung */ + id: number & Positive, + /** @description Benutzer löschen? */ + remove: boolean = false + ) { + console.log('id', id, typeof id); + } +} +``` + +In der Hilfeansicht erscheint diese Beschreibung nach dem Flag bzw. Argument: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test ID + +ARGUMENTS + ID The users identifier + +OPTIONS + --remove Delete the user? +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/app/configuration.md b/website/src/translations/de/documentation/app/configuration.md new file mode 100644 index 000000000..fc2a878ab --- /dev/null +++ b/website/src/translations/de/documentation/app/configuration.md @@ -0,0 +1,265 @@ +# Konfiguration + +In Deepkit-Anwendungen können Module und Ihre Anwendung Konfigurationsoptionen haben. +Eine Konfiguration kann z. B. aus Datenbank-URLs, Passwörtern, IPs usw. bestehen. Services, HTTP/RPC/CLI-Controller und Template-Funktionen können diese Konfigurationsoptionen über Dependency Injection auslesen. + +Eine Konfiguration kann definiert werden, indem eine Class mit Properties definiert wird. Dies ist eine typesafe Möglichkeit, eine Konfiguration für Ihre gesamte Anwendung zu definieren, und ihre Werte werden automatisch serialisiert und validiert. + +## Beispiel + +```typescript +import { MinLength } from '@deepkit/type'; +import { App } from '@deepkit/app'; + +class Config { + pageTitle: string & MinLength<2> = 'Cool site'; + domain: string = 'example.com'; + debug: boolean = false; +} + +const app = new App({ + config: Config +}); + + +app.command('print-config', (config: Config) => { + console.log('config', config); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Hello from Cool site via example.com +``` + +Wenn kein Konfigurations-Loader verwendet wird, werden die Default-Werte verwendet. Um die Konfiguration zu ändern, können Sie entweder die `app.configure({domain: 'localhost'})` Method verwenden oder einen Environment Configuration Loader benutzen. + +## Konfigurationswerte setzen + +Standardmäßig werden keine Werte überschrieben, daher werden Default-Werte verwendet. Es gibt mehrere Möglichkeiten, Konfigurationswerte zu setzen. + +* Über `app.configure({})` +* Umgebungsvariablen für jede Option +* Umgebungsvariable via JSON +* dotenv-Dateien + +Sie können mehrere Methoden gleichzeitig verwenden, um die Konfiguration zu laden. Die Reihenfolge, in der sie aufgerufen werden, ist wichtig. + +### Umgebungsvariablen + +Um jede Konfigurationsoption über ihre eigene Umgebungsvariable setzen zu können, verwenden Sie `loadConfigFromEnv`. Der Default-Prefix ist `APP_`, kann aber geändert werden. Außerdem lädt es automatisch `.env`-Dateien. Standardmäßig wird eine Uppercase-Naming-Strategie verwendet, die Sie ebenfalls ändern können. + +Für Konfigurationsoptionen wie `pageTitle` oben können Sie `APP_PAGE_TITLE="Other Title"` verwenden, um den Wert zu ändern. + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({prefix: 'APP_'}) + .run(); +``` + +```sh +APP_PAGE_TITLE="Other title" ts-node app.ts server:start +``` + +### JSON-Umgebungsvariable + +Um mehrere Konfigurationsoptionen über eine einzelne Umgebungsvariable zu ändern, verwenden Sie `loadConfigFromEnvVariable`. Das erste Argument ist der Name der Umgebungsvariablen. + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnvVariable('APP_CONFIG') + .run(); +``` + +```sh +APP_CONFIG='{"pageTitle": "Other title"}' ts-node app.ts server:start +``` + +### DotEnv-Dateien + +Um mehrere Konfigurationsoptionen über eine dotenv-Datei zu ändern, verwenden Sie `loadConfigFromEnv`. Das erste Argument ist entweder ein Pfad zu einer dotenv (relativ zu `cwd`) oder mehrere Pfade. Handelt es sich um ein Array, wird jeder Pfad ausprobiert, bis eine existierende Datei gefunden wird. + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({envFilePath: ['production.dotenv', 'dotenv']}) + .run(); +``` + +```sh +$ cat dotenv +APP_PAGE_TITLE=Other title +$ ts-node app.ts server:start +``` + +### Modulkonfiguration + +Jedes importierte Modul kann einen Modulnamen haben. Dieser Name wird für die oben verwendeten Konfigurationspfade benutzt. + +Zum Beispiel ist beim Konfigurieren per Umgebungsvariablen der Pfad für die Option port des `FrameworkModule` `FRAMEWORK_PORT`. Alle Namen werden standardmäßig in Großbuchstaben geschrieben. Wird ein Prefix `APP_` verwendet, kann der Port wie folgt geändert werden: + +```sh +$ APP_FRAMEWORK_PORT=9999 ts-node app.ts server:start +2021-06-12T18:59:26.363Z [LOG] Start HTTP server, using 1 workers. +2021-06-12T18:59:26.365Z [LOG] HTTP MyWebsite +2021-06-12T18:59:26.366Z [LOG] GET / helloWorld +2021-06-12T18:59:26.366Z [LOG] HTTP listening at http://localhost:9999/ +``` + +In dotenv-Dateien wäre es ebenfalls `APP_FRAMEWORK_PORT=9999`. + +In JSON-Umgebungsvariablen über `loadConfigFromEnvVariable('APP_CONFIG')` hingegen entspricht es der Struktur der eigentlichen Konfigurations-Class. `framework` wird zu einem Object. + +```sh +$ APP_CONFIG='{"framework": {"port": 9999}}' ts-node app.ts server:start +``` + +Das funktioniert für alle Module gleich. Für die Konfigurationsoption Ihrer Anwendung (`new App`) ist kein Modul-Prefix erforderlich. + + +## Konfigurationsklasse + +```typescript +import { MinLength } from '@deepkit/type'; + +export class Config { + title!: string & MinLength<2>; //dadurch wird es erforderlich und muss angegeben werden + host?: string; + + debug: boolean = false; //Default-Werte werden ebenfalls unterstützt +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} +``` + +Die Werte für die Konfigurationsoptionen können entweder im Konstruktor des Moduls, mit der `.configure()` Method, oder über Configuration Loader bereitgestellt werden (z. B. Environment Variable Loader). + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [new MyModule({title: 'Hello World'})], +}).run(); +``` + +Um die Konfigurationsoptionen eines importierten Moduls dynamisch zu ändern, können Sie den `process` Hook verwenden. Dies ist ein guter Ort, um entweder Konfigurationsoptionen weiterzureichen oder ein importiertes Modul abhängig von der aktuellen Modulkonfiguration oder anderen Modulinstanz-Informationen einzurichten. + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} +``` + +Auf Anwendungsebene funktioniert es ein wenig anders: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +Wenn das Root-Anwendungsmodul aus einem regulären Modul erstellt wird, funktioniert es ähnlich wie bei regulären Modulen. + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## Konfigurationswerte lesen + +Um eine Konfigurationsoption in einem Service zu verwenden, können Sie normale Dependency Injection nutzen. Es ist möglich, entweder das gesamte Konfigurations-Object, einen einzelnen Wert oder einen Teil der Konfiguration zu injizieren. + +### Teilmenge + +Um nur eine Teilmenge der Konfigurationswerte zu injizieren, verwenden Sie den `Pick` Type. + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private config: Pick { + logger.log('Hello World!'); +}); +``` + +```typescript +@cli.controller('test', { + description: 'My super first command' +}) +class TestCommand { + constructor(protected logger: Logger) { + } + + async execute() { + this.logger.log('Hello World!'); + } +} + +new App({ + providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport]}], + controllers: [TestCommand] +}).run(); +``` + +Sie können beliebig viele Abhängigkeiten definieren. Der Dependency-Injection-Container wird sie automatisch auflösen. \ No newline at end of file diff --git a/website/src/translations/de/documentation/app/events.md b/website/src/translations/de/documentation/app/events.md new file mode 100644 index 000000000..e270fa61a --- /dev/null +++ b/website/src/translations/de/documentation/app/events.md @@ -0,0 +1,248 @@ +# Event-System + +Ein Event-System ermöglicht es Application-Komponenten innerhalb desselben Prozesses, durch Senden und Lauschen von Events zu kommunizieren. Dies unterstützt die Modularisierung des Codes, indem der Nachrichtenaustausch zwischen Functions erleichtert wird, die nicht unbedingt direkt voneinander wissen. + +Die Application oder Library bietet die Möglichkeit, an bestimmten Stellen während ihres Ablaufs zusätzliche Functions auszuführen. Diese zusätzlichen Functions registrieren sich als sogenannte "Event Listener". + +Ein Event kann verschiedene Formen annehmen: + +- Die Application startet oder fährt herunter. +- Ein neuer User wird erstellt oder gelöscht. +- Ein Error wird geworfen. +- Eine neue HTTP-Request wird empfangen. + +Das Deepkit Framework und seine zugehörigen Libraries bieten eine Reihe von Events, auf die Nutzer lauschen und reagieren können. Nutzer haben jedoch auch die Flexibilität, beliebig viele benutzerdefinierte Events zu erstellen, um die Application modular zu erweitern. + +## Verwendung + +Wenn du eine Deepkit App verwendest, ist das Event-System bereits enthalten und einsatzbereit. + +```typescript +import { App, onAppExecute } from '@deepkit/app'; + +const app = new App(); + +app.listen(onAppExecute, async (event) => { + console.log('MyEvent triggered!'); +}); + +app.run(); +``` + +Events können entweder über die `listen()`-Methode oder über eine Class mit dem `@eventDispatcher.listen` Decorator registriert werden: + +```typescript +import { App, onAppExecute } from '@deepkit/app'; +import { eventDispatcher } from '@deepkit/event'; + +class MyListener { + @eventDispatcher.listen(onAppExecute) + onMyEvent(event: typeof onAppExecute.event) { + console.log('MyEvent triggered!'); + } +} + +const app = new App({ + listeners: [MyListener], +}); +app.run(); +``` + +## Event Token + +Im Kern von Deepkits Event-System stehen Event Tokens. Dabei handelt es sich um einzigartige Objekte, die sowohl die Event-ID als auch den Typ des Events spezifizieren. Ein Event Token erfüllt zwei Hauptzwecke: + +- Es fungiert als Auslöser für ein Event. +- Es dient zum Lauschen auf das Event, das es auslöst. + +Wenn ein Event über ein Event Token initiiert wird, gilt der Eigentümer dieses Tokens effektiv als Quelle des Events. Das Token bestimmt die mit dem Event verknüpften Daten und legt fest, ob asynchrone Event Listener verwendet werden können. + +```typescript +import { EventToken } from '@deepkit/event'; + +const MyEvent = new EventToken('my-event'); + +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); + +// Auslösen über App-Referenz +await app.dispatch(MyEvent); + +// Oder den EventDispatcher verwenden; der DI-Container der App injiziert ihn automatisch +app.command('test', async (dispatcher: EventDispatcher) => { + await dispatcher.dispatch(MyEvent); +}); +``` + +### Benutzerdefinierte Event-Daten erstellen: + +Mit `DataEventToken` aus @deepkit/event: + +```typescript +import { DataEventToken } from '@deepkit/event'; + +class User { +} + +const MyEvent = new DataEventToken('my-event'); +``` + +BaseEvent erweitern: + +```typescript +class MyEvent extends BaseEvent { + user: User = new User; +} + +const MyEventToken = new EventToken('my-event'); +``` + +## Funktionale Listener + +Funktionale Listener ermöglichen es, einen einfachen Function-Callback direkt beim Dispatcher zu registrieren. So geht's: + +```typescript +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +``` + +Wenn du zusätzliche Arguments wie `logger: Logger` einführen möchtest, werden sie dank der Dependency Injection und Deepkits Runtime Type Reflection automatisch injiziert. + +```typescript +app.listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); +}); +``` + +Beachte, dass das erste Argument das Event selbst sein muss. Dieses Argument kann nicht weggelassen werden. + +Wenn du `@deepkit/app` verwendest, kannst du auch app.listen() verwenden, um einen funktionalen Listener zu registrieren. + +```typescript +import { App } from '@deepkit/app'; + +new App() + .listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## Klassenbasierte Listener + +Class Listener sind Classes mit Decorators. Sie bieten eine strukturierte Möglichkeit, auf Events zu lauschen. + +```typescript +import { App } from '@deepkit/app'; + +class MyListener { + @eventDispatcher.listen(UserAdded) + onUserAdded(event: typeof UserAdded.event) { + console.log('User added!', event.user.username); + } +} + +new App({ + listeners: [MyListener], +}).run(); +``` + +Für Class Listener funktioniert Dependency Injection entweder über die Method Arguments oder den Constructor. + +## Dependency Injection + +Das Event-System von Deepkit verfügt über einen leistungsfähigen Dependency-Injection-Mechanismus. Bei funktionalen Listenern werden zusätzliche Arguments dank des Runtime Type Reflection-Systems automatisch injiziert. Ebenso unterstützen klassenbasierte Listener Dependency Injection entweder über den Constructor oder über Method Arguments. + +Beispielsweise wird bei einem funktionalen Listener die passende Logger-Instanz automatisch bereitgestellt, wenn du ein Argument wie `logger: Logger` hinzufügst und die Function aufgerufen wird. + +```typescript +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +new App() + .listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## Event Propagation + +Jedes Event-Objekt verfügt über eine stop()-Function, mit der du die Propagation des Events steuern kannst. Wenn ein Event angehalten wird, werden keine nachfolgenden Listener (in der Reihenfolge, in der sie hinzugefügt wurden) mehr ausgeführt. Dies ermöglicht eine fein granulare Kontrolle über die Ausführung und Verarbeitung von Events, insbesondere in Szenarien, in denen bestimmte Bedingungen das Anhalten der Event-Verarbeitung erfordern. + +Zum Beispiel: + +```typescript +dispatcher.listen(MyEventToken, (event) => { + if (someCondition) { + event.stop(); + } + // Weitere Verarbeitung +}); +``` + +Mit dem Event-System des Deepkit Frameworks können Developer modular, skalierbar und wartbar entwickeln. Das Verständnis des Event-Systems bietet die Flexibilität, das Verhalten der Application an bestimmte Events oder Bedingungen anzupassen. + +## Framework-Events + +Das Deepkit Framework selbst stellt mehrere Events des Application Servers bereit, auf die du lauschen kannst. + +_Funktionaler Listener_ + +```typescript +import { onServerMainBootstrap } from '@deepkit/framework'; +import { onAppExecute } from '@deepkit/app'; + +new App({ + imports: [new FrameworkModule] +}) + .listen(onAppExecute, (event) => { + console.log('Command about to execute'); + }) + .listen(onServerMainBootstrap, (event) => { + console.log('Server started'); + }) + .run(); +``` + +| Name | Beschreibung | +|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| onServerBootstrap | Wird nur einmal beim Bootstrap des Application Servers aufgerufen (für Main Process und Worker). | +| onServerBootstrapDone | Wird nur einmal beim Bootstrap des Application Servers aufgerufen (für Main Process und Worker), sobald der Application Server gestartet wurde. | +| onServerMainBootstrap | Wird nur einmal beim Bootstrap des Application Servers aufgerufen (im Main Process). | +| onServerMainBootstrapDone | Wird nur einmal beim Bootstrap des Application Servers aufgerufen (im Main Process), sobald der Application Server gestartet wurde. | +| onServerWorkerBootstrap | Wird nur einmal beim Bootstrap des Application Servers aufgerufen (im Worker Process). | +| onServerWorkerBootstrapDone | Wird nur einmal beim Bootstrap des Application Servers aufgerufen (im Worker Process), sobald der Application Server gestartet wurde. | +| onServerShutdownEvent | Wird aufgerufen, wenn der Application Server herunterfährt (im Master Process und in jedem Worker). | +| onServerMainShutdown | Wird aufgerufen, wenn der Application Server im Main Process herunterfährt. | +| onServerWorkerShutdown | Wird aufgerufen, wenn der Application Server im Worker Process herunterfährt. | +| onAppExecute | Wenn ein Command gleich ausgeführt wird. | +| onAppExecuted | Wenn ein Command erfolgreich ausgeführt wurde. | +| onAppError | Wenn ein Command nicht ausgeführt werden konnte. | +| onAppShutdown | Wenn die Application kurz vor dem Herunterfahren steht. | + +## Low-Level-API + +Unten ein Beispiel der Low-Level-API aus @deepkit/event. Wenn du die Deepkit App verwendest, werden Event Listener nicht direkt über den EventDispatcher registriert, sondern über Modules. Du kannst die Low-Level-API aber dennoch verwenden, wenn du möchtest. + +```typescript +import { EventDispatcher, EventToken } from '@deepkit/event'; + +// Das erste Argument kann ein Injector-Kontext sein, um Dependencies für Dependency Injection aufzulösen +const dispatcher = new EventDispatcher(); +const MyEvent = new EventToken('my-event'); + +dispatcher.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +dispatcher.dispatch(MyEvent); + +``` + +### Installation + +Das Event-System ist in @deepkit/app enthalten. Wenn du es standalone verwenden möchtest, kannst du es manuell installieren: + +Siehe [Event](../event.md) für Installationsanweisungen. diff --git a/website/src/translations/de/documentation/app/logger.md b/website/src/translations/de/documentation/app/logger.md new file mode 100644 index 000000000..2c499917d --- /dev/null +++ b/website/src/translations/de/documentation/app/logger.md @@ -0,0 +1,130 @@ +# Logger + +Deepkit Logger ist eine eigenständige Bibliothek mit einer primären Logger Class, die du zum Protokollieren verwenden kannst. Diese Class ist im Dependency Injection-Container deiner Deepkit-Anwendung automatisch verfügbar. + +Die `Logger` Class hat mehrere Methods, die sich jeweils wie `console.log` verhalten. + +| Name | Log Level | Level-ID | +|-----------------|---------------------|----------| +| logger.error() | Error | 1 | +| logger.warning()| Warning | 2 | +| logger.log() | Standard-Log | 3 | +| logger.info() | Spezielle Informationen | 4 | +| logger.debug() | Debug-Informationen | 5 | + + +Standardmäßig hat ein Logger Level `info`, d. h. er verarbeitet nur info-Nachrichten und höher (d. h. log, warning, error, aber nicht debug). Um das Log Level zu ändern, rufe z. B. `logger.level = 5` auf. + +## Verwendung in der Anwendung + +Um den Logger in deiner Deepkit-Anwendung zu verwenden, kannst du `Logger` einfach in deine Services oder Controller injecten. + +```typescript +import { Logger } from '@deepkit/logger'; +import { App } from '@deepkit/app'; + +const app = new App(); +app.command('test', (logger: Logger) => { + logger.log('This is a log message'); +}); + +app.run(); +``` + +## Farben + +Der Logger unterstützt farbige Log-Nachrichten. Du kannst Farben bereitstellen, indem du XML-Tags verwendest, die den Text umschließen, der farbig erscheinen soll. + +```typescript +const username = 'Peter'; +logger.log(`Hi ${username}`); +``` + +Für Transporter, die keine Farben unterstützen, wird die Farbinformation automatisch entfernt. Im Standard-Transporter (`ConsoleTransport`) wird die Farbe angezeigt. Folgende Farben sind verfügbar: `black`, `red`, `green`, `blue`, `cyan`, `magenta`, `white` und `grey`/`gray`. + +## Transporter + +Du kannst einen einzelnen Transporter oder mehrere Transporter konfigurieren. In einer Deepkit-Anwendung ist der `ConsoleTransport`-Transporter automatisch konfiguriert. Um zusätzliche Transporter zu konfigurieren, kannst du [Setup-Aufrufe](dependency-injection.md#di-setup-calls) verwenden: + +```typescript +import { Logger, LoggerTransport } from '@deepkit/logger'; + +export class MyTransport implements LoggerTransport { + write(message: string, level: LoggerLevel, rawMessage: string) { + process.stdout.write(JSON.stringify({message: rawMessage, level, time: new Date}) + '\n'); + } + + supportsColor() { + return false; + } +} + +new App() + .setup((module, config) => { + module.configureProvider(v => v.addTransport(new MyTransport)); + }) + .run(); +``` + +Um alle Transporter durch einen neuen Satz von Transportern zu ersetzen, verwende `setTransport`: + +```typescript +import { Logger } from '@deepkit/logger'; + +new App() +.setup((module, config) => { + module.configureProvider(v => v.setTransport([new MyTransport])); +}) +.run(); +``` + +```typescript +import { Logger, JSONTransport } from '@deepkit/logger'; + +new App() + .setup((module, config) => { + module.configureProvider(v => v.setTransport([new JSONTransport])); + }) + .run(); +``` + +## Scoped Logger + +Scoped Logger fügen jedem Log-Eintrag einen beliebigen Bereichsnamen hinzu, was dabei helfen kann festzustellen, aus welchem Teilbereich deiner Anwendung der Log-Eintrag stammt. + +```typescript +const scopedLogger = logger.scoped('database'); +scopedLogger.log('Query', query); +``` + +Es gibt auch einen `ScopedLogger` Type, den du verwenden kannst, um Scoped Logger in deine Services zu injecten. + +```typescript +import { ScopedLogger } from '@deepkit/logger'; + +class MyService { + constructor(protected logger: ScopedLogger) {} + doSomething() { + this.logger.log('This is wild'); + } +} +``` + +Alle Nachrichten eines Scoped Loggers werden nun mit dem Scope-Namen `MyService` vorangestellt. + +## Formatter + +Mit Formattern kannst du das Nachrichtenformat ändern, z. B. den Zeitstempel hinzufügen. Wenn eine Anwendung über `server:start` gestartet wird, wird automatisch ein `DefaultFormatter` hinzugefügt (der Zeitstempel, Bereich und Log Level hinzufügt), wenn kein anderer Formatter verfügbar ist. + +## Kontextdaten + +Um Kontextdaten zu einem Log-Eintrag hinzuzufügen, füge als letztes Argument ein einfaches Objektliteral hinzu. Nur Log-Aufrufe mit mindestens zwei Argumenten können Kontextdaten enthalten. + +```typescript +const query = 'SELECT *'; +const user = new User; +logger.log('Query', {query, user}); //letztes Argument sind Kontextdaten +logger.log('Another', 'wild log entry', query, {user}); //letztes Argument sind Kontextdaten + +logger.log({query, user}); //dies wird nicht als Kontextdaten behandelt. +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/app/modules.md b/website/src/translations/de/documentation/app/modules.md new file mode 100644 index 000000000..8ec54a2ec --- /dev/null +++ b/website/src/translations/de/documentation/app/modules.md @@ -0,0 +1,517 @@ +# Module + +Deepkit ist hochgradig modular und ermöglicht es Ihnen, Ihre Anwendung in mehrere praktische Module aufzuteilen. Jedes Modul hat seinen eigenen Dependency-Injection-Subcontainer (der alle übergeordneten Provider erbt), Konfiguration, Befehle und vieles mehr. +Im Kapitel [Erste Schritte](../framework.md) haben Sie bereits ein Modul erstellt – das Root-Modul. `new App` nimmt fast die gleichen Argumente wie ein Modul entgegen, da es im Hintergrund automatisch das Root-Modul für Sie erstellt. + +Sie können dieses Kapitel überspringen, wenn Sie nicht planen, Ihre Anwendung in Submodule aufzuteilen, oder wenn Sie nicht vorhaben, ein Modul als Paket für andere verfügbar zu machen. + +Ein Modul kann entweder als Klassenmodul oder als Funktionsmodul definiert werden. + +```typescript title=Klassenmodul +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + //gleiche Optionen wie new App({}) + providers: [MyService] +}) { +} +``` + +```typescript title=Funktionsmodul +import { AppModule } from '@deepkit/app'; + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addProvider(MyService); + }; +} +``` + +Dieses Modul kann dann in Ihre Anwendung oder andere Module importiert werden. + +```typescript +import { MyModule, myModule } from './module.ts' + +new App({ + imports: [ + new MyModule(), //Klassenmodul importieren + myModule(), //Funktionsmodul importieren + ] +}).run(); +``` + +Sie können diesem Modul nun Funktionen hinzufügen, wie Sie es auch mit `App` tun würden. Die Argumente von `createModule` sind dieselben, mit der Ausnahme, dass Importe in einer Moduldefinition nicht verfügbar sind. +Für Funktionsrouten können Sie die Methoden von `AppModule` verwenden, um es dynamisch basierend auf Ihren eigenen Optionen zu konfigurieren. + +Fügen Sie HTTP/RPC/CLI-Controller, Services, eine Konfiguration, Event-Listener und verschiedene Modul-Hooks hinzu, um Module dynamischer zu machen. + +## Controller + +Module können Controller definieren, die von anderen Modulen verarbeitet werden. Wenn Sie beispielsweise einen Controller mit Decorators aus dem Paket `@deepkit/http` hinzufügen, wird dessen `HttpModule` dies erkennen und die gefundenen Routen in seinem Router registrieren. Ein einzelner Controller kann mehrere solcher Decorators enthalten. Es liegt beim Autor des Moduls, der Ihnen diese Decorators zur Verfügung stellt, wie er die Controller verarbeitet. + +In Deepkit gibt es drei Pakete, die solche Controller verarbeiten: HTTP, RPC und CLI. Siehe deren jeweilige Kapitel, um mehr zu erfahren. Unten ist ein Beispiel für einen HTTP-Controller: + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +class MyHttpController { + @http.GET('/hello) + hello() { + return 'Hello world!'; + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] +}) { +} + + +//gleiches ist für App möglich +new App({ + controllers: [MyHttpController] +}).run(); +``` + +## Provider + +Wenn Sie in Ihrer Anwendung einen Provider im Abschnitt `providers` definieren, ist er in der gesamten Anwendung zugänglich. Bei Modulen jedoch werden diese Provider automatisch im Dependency-Injection-Subcontainer dieses Moduls gekapselt. Sie müssen jeden Provider manuell exportieren, um ihn für ein anderes Modul oder Ihre Anwendung verfügbar zu machen. + +Um mehr darüber zu erfahren, wie Provider funktionieren, lesen Sie das Kapitel [Abhängigkeitsinjektion](../dependency-injection.md). + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +export class HelloWorldService { + helloWorld() { + return 'Hello there!'; + } +} + +class MyHttpController { + constructor(private helloService: HelloWorldService) { + } + + @http.GET('/hello) + hello() { + return this.helloService.helloWorld(); + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addController(MyHttpController); + module.addProvider(HelloWorldService); + }; +} + +//gleiches ist für App möglich +new App({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}).run(); +``` + +Wenn ein Benutzer dieses Modul importiert, hat er keinen Zugriff auf `HelloWorldService`, da es im Sub-Dependency-Injection-Container von `MyModule` gekapselt ist. + +## Exporte + +Um Provider im Modul des Importeurs verfügbar zu machen, können Sie das Token des Providers in `exports` aufnehmen. Dies verschiebt den Provider im Wesentlichen eine Ebene nach oben in den Dependency-Injection-Container des übergeordneten Moduls – des Importeurs. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + exports: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addExport(HelloWorldService); + }; +} +``` + +Wenn Sie andere Provider wie `FactoryProvider`, `UseClassProvider` usw. haben, sollten Sie in den Exports trotzdem nur den Class Type verwenden. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] + providers: [ + { provide: HelloWorldService, useValue: new HelloWorldService } + ], + exports: [HelloWorldService], +}) { +} +``` + +Wir können dieses Modul nun importieren und seinen exportierten Service in unserem Anwendungscode verwenden. + +```typescript +import { App } from '@deepkit/app'; +import { cli, Command } from '@deepkit/app'; +import { HelloWorldService, MyModule } from './my-module'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected helloWorld: HelloWorldService) { + } + + async execute() { + this.helloWorld.helloWorld(); + } +} + +new App({ + controllers: [TestCommand], + imports: [ + new MyModule(), + ] +}).run(); +``` + +Lesen Sie das Kapitel [Abhängigkeitsinjektion](../dependency-injection.md), um mehr zu erfahren. + + +### Konfigurationsschema + +Ein Modul kann typsichere Konfigurationsoptionen haben. Die Werte dieser Optionen können teilweise oder vollständig in Services dieses Moduls injiziert werden, indem einfach die Klassenreferenz oder Typfunktionen wie `Partial` verwendet werden. Um ein Konfigurationsschema zu definieren, schreiben Sie eine Klasse mit Properties. + +```typescript +export class Config { + title!: string; //erforderlich und muss bereitgestellt werden + host?: string; //optional + + debug: boolean = false; //Standardwerte werden ebenfalls unterstützt +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} + +export function myModule(options: Partial = {}) { + return (module: AppModule) => { + module.setConfigDefinition(Config).configure(options); + }; +} +``` + +Konfigurationswerte können entweder über den Konstruktor Ihres Moduls, mit der Methode `.configure()`, oder über Konfigurationslader (z. B. Umgebungsvariablen-Lader) bereitgestellt werden. + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [ + new MyModule({title: 'Hello World'}), + myModule({title: 'Hello World'}), + ], +}).run(); +``` + +Um die Konfigurationsoptionen eines importierten Moduls dynamisch zu ändern, können Sie den `process`-Modul-Hook verwenden. Dies ist ein guter Ort, um entweder Konfigurationsoptionen umzuleiten oder ein importiertes Modul abhängig von der aktuellen Moduls-Konfiguration oder anderen Modulinstanz-Informationen einzurichten. + + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +export function myModule(options: Partial = {}) { + return (module: AppModule) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }; +} +``` + +Auf Anwendungsebene funktioniert es etwas anders: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +Wenn das Root-Anwendungsmodul aus einem regulären Modul erstellt wird, funktioniert es ähnlich wie bei regulären Modulen. + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## Modulname + +Alle Konfigurationsoptionen können auch über Umgebungsvariablen geändert werden. Dies funktioniert nur, wenn dem Modul ein Name zugewiesen wurde. Ein Modulname kann über `createModule` definiert und später bei der Instanzerzeugung dynamisch geändert werden. Letzteres Muster ist nützlich, wenn Sie dasselbe Modul zweimal importiert haben und zwischen ihnen unterscheiden möchten, indem Sie einen neuen Namen setzen. + +```typescript +export class MyModule extends createModuleClass({ + name: 'my' +}) { +} + +export function myModule(options: Partial = {}) { + return (module: AppModule) => { + module.name = 'my'; + }; +} +``` + +```typescript +import { MyModule } from './module'; + +new App({ + imports: [ + new MyModule(), //'my' ist der Standardname + new MyModule().rename('my2'), //'my2' ist jetzt der neue Name + ] +}).run(); +``` + +Siehe das Kapitel [Konfiguration](./configuration.md) für weitere Informationen darüber, wie Konfigurationsoptionen aus Umgebungsvariablen oder .env-Dateien geladen werden. + +## Importe + +Module können andere Module importieren, um ihre Funktionalität zu erweitern. In `App` können Sie andere Module im Moduldefinitionsobjekt über `imports: []` importieren: + +```typescript +new App({ + imports: [new Module] +}).run(); +``` + +In regulären Modulen ist das nicht möglich, da das Modul in der Objekdefinitionsobjekt-Instanz global werden würde, was normalerweise nicht erwünscht ist. Stattdessen können Module innerhalb des Moduls selbst über die Eigenschaft `imports` instanziiert werden, sodass Instanzen jedes importierten Moduls für jede neue Instanz Ihres Moduls erstellt werden. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + imports = [new OtherModule()]; +} + +export function myModule() { + return (module: AppModule) => { + module.addImport(new OtherModule()); + }; +} +``` + +Sie können Module auch dynamisch basierend auf der Konfiguration mit dem Hook `process` importieren. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + process() { + if (this.config.xEnabled) { + this.addImport(new OtherModule({ option: 'value' }); + } + } +} + +export function myModule(option: { xEnabled?: boolean } = {}) { + return (module: AppModule) => { + if (option.xEnabled) { + module.addImport(new OtherModule()); + } + }; +} +``` + +## Hooks + +Der Service-Container lädt alle Module in der Reihenfolge, in der sie importiert wurden, beginnend beim Root-/Anwendungsmodul. + +Während dieses Prozesses führt der Service-Container auch alle registrierten Konfigurationslader aus, ruft `setupConfig`-Callbacks auf und validiert anschließend die Konfigurationsobjekte jedes Moduls. + +Der gesamte Prozess des Ladens des Service-Containers ist wie folgt: + +1. Für jedes Modul `T` (beginnend beim Root) + 1. Konfigurationslader ausführen `ConfigLoader.load(T)`. + 2. `T.setupConfig()` aufrufen. + 3. Konfiguration von `T` validieren. Abbrechen, wenn ungültig. + 4. `T.process()` aufrufen. + Hier kann das Modul sich selbst basierend auf gültigen Konfigurationsoptionen modifizieren. Neue Importe, Provider usw. hinzufügen. + 5. 1. für jedes importierte Modul von `T` wiederholen. +3. Alle registrierten Module finden. +4. Jedes gefundene Modul `T` verarbeiten. + 1. Middlewares von `T` registrieren. + 2. Listener von `T` im Event-Dispatcher registrieren. + 3. Für alle aus 2. gefundenen Module `Module.processController(T, controller)` aufrufen. + 4. Für alle aus 2. gefundenen Module `Module.processProvider(T, token, provider)` aufrufen. + 5. 3. für jedes importierte Modul von `T` wiederholen. +5. `T.postProcess()` auf allen Modulen ausführen. +6. Die Bootstrap-Klasse auf allen Modulen instanziieren. +7. Der Dependency-Injection-Container ist nun aufgebaut. + +Um Hooks zu verwenden, können Sie die Methoden `process`, `processProvider`, `postProcess` in Ihrer Modulkasse registrieren. + +```typescript +import { createModuleClass, AppModule } from '@deepkit/app'; +import { isClass } from '@deepkit/core'; +import { ProviderWithScope, Token } from '@deepkit/injector'; + +export class MyModule extends createModuleClass({}) { + imports = [new FrameworkModule()]; + + //zuerst ausgeführt + process() { + //this.config enthält das vollständig validierte Konfigurationsobjekt. + if (this.config.environment === 'development') { + this.getImportedModuleByClass(FrameworkModule).configure({ debug: true }); + } + this.addModule(new AnotherModule); + this.addProvider(Service); + + //ruft zusätzliche Setup-Methoden auf. + //In diesem Fall wird 'method1' mit den angegebenen Argumenten aufgerufen, wenn + //Service vom Dependency-Injection-Container instanziiert wird. + this.configureProvider(v => v.method1(this.config.value)); + } + + //für jeden gefundenen Controller in allen Modulen ausgeführt + processController(module: AppModule, controller: ClassType) { + //HttpModule prüft beispielsweise bei jedem Controller, ob + //ein @http Decorator verwendet wurde, und extrahiert in diesem Fall alle Routen- + //informationen und legt sie im Router ab. + } + + //für jeden gefundenen Provider in allen Modulen ausgeführt + processProvider(module: AppModule, token: Token, provider: ProviderWithScope) { + //FrameworkModule sucht beispielsweise nach bereitgestellten Tokens, die von deepkit/orm Database erweitern, + //und registriert sie automatisch in einer DatabaseRegistry, sodass sie in den Migration-CLI-Befehlen + //und im Framework-Debugger verwendet werden können. + } + + //wird ausgeführt, wenn alle Module verarbeitet wurden. + //Letzte Chance, Provider über module.configureProvider basierend auf + //Informationen aus process/processProvider einzurichten. + postProcess() { + + } +} +``` + +## Zustandsbehaftete Module + +Da jedes Modul explizit mit `new Module` instanziiert wird, kann das Modul einen Zustand haben. Dieser Zustand kann in den Dependency-Injection-Container injiziert werden, sodass er für Services verfügbar ist. + +Betrachten Sie als Beispiel den HttpModule-Anwendungsfall. Es prüft jeden registrierten Controller in der gesamten Anwendung auf bestimmte @http-Decorators und legt den Controller in ein Register, wenn vorhanden. Dieses Register wird in den Router injiziert, der beim Instanziieren alle Routeninformationen dieser Controller extrahiert und registriert. + +```typescript +class Registry { + protected controllers: { module: AppModule, classType: ClassType }[] = []; + + register(module: AppModule, controller: ClassType) { + this.controllers.push({ module, classType: controller }); + } + + get(classType: ClassType) { + const controller = this.controllers.find(v => v.classType === classType); + if (!controller) throw new Error('Controller unknown'); + return controller; + } +} + +class Router { + constructor( + protected injectorContext: InjectorContext, + protected registry: Registry + ) { + } + + getController(classType: ClassType) { + //finde classType und Modul für den gegebenen Controller-classType + const controller = this.registry.get(classType); + + //hier wird der Controller instanziiert. Wenn er bereits + //instanziiert wurde, wird die alte Instanz zurückgegeben (falls der Provider nicht transient: true war) + return injector.get(controller.classType, controller.module); + } +} + +class HttpModule extends createModuleClass({ + providers: [Router], + exports: [Router], +}) { + protected registry = new Registry; + + process() { + this.addProvider({ provide: Registry, useValue: this.registry }); + } + + processController(module: AppModule, controller: ClassType) { + //Controller müssen vom Controller-Verbraucher in die Provider des Moduls aufgenommen werden + if (!module.isProvided(controller)) module.addProvider(controller); + this.registry.register(module, controller); + } +} + +class MyController {} + +const app = new App({ + controllers: [MyController], + imports: [new HttpModule()] +}); + +const myController = app.get(Router).getController(MyController); +``` + +## Für Root + +Die Eigenschaft `root` ermöglicht es Ihnen, den Dependency-Injection-Container eines Moduls in den Container der Root-Anwendung zu verschieben. Dadurch wird jeder Service aus dem Modul automatisch in der Root-Anwendung selbst verfügbar. Im Grunde verschiebt dies jeden Provider (Controller, Event-Listener, Provider) in den Root-Container. Dies könnte zu Abhängigkeitskonflikten führen und sollte daher nur für ein Modul verwendet werden, das wirklich nur Globals enthält. Sie sollten stattdessen bevorzugt jeden Provider manuell exportieren. + +Wenn Sie eine Bibliothek erstellen, die von vielen Modulen verwendet werden kann, sollten Sie `root` vermeiden, da es mit Provider-Tokens aus anderen Bibliotheken kollidieren könnte. Wenn diese Bibliotheksmodul beispielsweise ein `foo`-Modul importiert, das einen Service definiert, und Sie einige Services nach Ihren Bedürfnissen rekonfigurieren, und die Anwendung des Benutzers dasselbe `foo`-Modul importiert, erhält der Benutzer Ihre rekonfigurierten Services. Für viele einfachere Anwendungsfälle mag das jedoch in Ordnung sein. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + root = true; +} +``` + +Sie können die Eigenschaft `root` eines Drittanbieter-Moduls auch mithilfe von `forRoot()` ändern. + +```typescript +new App({ + imports: [new ThirdPartyModule().forRoot()], +}).run(); +``` + +## Injector-Kontext + +Der InjectorContext ist der Dependency-Injection-Container. Er ermöglicht es Ihnen, Services aus Ihren eigenen oder anderen Modulen anzufordern/zu instanziieren. Dies ist notwendig, wenn Sie beispielsweise einen Controller in `processControllers` gespeichert haben und ihn korrekt instanziieren möchten. \ No newline at end of file diff --git a/website/src/translations/de/documentation/app/services.md b/website/src/translations/de/documentation/app/services.md new file mode 100644 index 000000000..bf2599bbb --- /dev/null +++ b/website/src/translations/de/documentation/app/services.md @@ -0,0 +1,62 @@ +# Services + + +Service ist eine breite Kategorie, die jeden Wert, jede Function oder jedes Feature umfasst, das eine Anwendung benötigt. Ein Service ist typischerweise eine Class mit einem engen, klar definierten Zweck. Er sollte etwas Spezifisches tun und das gut. + +Ein Service in Deepkit (und in den meisten anderen JavaScript/TypeScript-Frameworks) ist eine einfache Class, die in einem Module mithilfe eines Providers registriert wird. Der einfachste Provider ist der Class Provider, der nur die Class selbst und sonst nichts angibt. Er wird dann zu einem Singleton im Dependency Injection Container des Modules, in dem er definiert wurde. + +Services werden vom Dependency Injection Container verwaltet und instanziiert und können daher mittels Constructor Injection oder Property Injection in anderen Services, in Controllern und Event Listenern importiert und verwendet werden. Siehe das Kapitel [Dependency Injection](../dependency-injection) für weitere Details. + +Um einen einfachen Service zu erstellen, schreiben Sie eine Class mit einem Zweck: + + +```typescript +export interface User { + username: string; +} + +export class UserManager { + users: User[] = []; + + addUser(user: User) { + this.users.push(user); + } +} +``` + +Und registrieren Sie ihn entweder in Ihrer Anwendung oder in einem Module: + +```typescript +new App({ + providers: [UserManager] +}).run(); +``` + +Danach können Sie diesen Service in Controllern, anderen Services oder Event Listenern verwenden. Verwenden wir diesen Service zum Beispiel in einem CLI-Command oder einer HTTP-Route: + + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + providers: [UserManager], + imports: [new FrameworkModule({debug: true})] +}); + +app.command('test', (userManager: UserManager) => { + for (const user of userManager.users) { + console.log('User: ', user.username); + } +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', (userManager: UserManager) => { + return userManager.users; +}) + +app.run(); +``` + +Ein Service ist ein grundlegender Baustein in Deepkit und ist nicht auf Classes beschränkt. Tatsächlich kann ein Service jeder Wert, jede Function oder jedes Feature sein, das von einer Anwendung benötigt wird. Um mehr zu erfahren, siehe Kapitel [Dependency Injection](../dependency-injection). \ No newline at end of file diff --git a/website/src/translations/de/documentation/broker.md b/website/src/translations/de/documentation/broker.md new file mode 100644 index 000000000..d62eee946 --- /dev/null +++ b/website/src/translations/de/documentation/broker.md @@ -0,0 +1,156 @@ +# Deepkit Broker + +Deepkit Broker ist eine High-Performance-Abstraktion für Message Queues, Message Bus, Event Bus, Pub/Sub, Key/Value Store, Cache und atomare Operationen. Alles im Sinne von Typsicherheit mit automatischer Serialisierung und Validierung, hoher Performance und Skalierbarkeit. + +Deepkit Broker ist Client und Server in einem. Er kann als eigenständiger Server verwendet werden oder als Client, um sich mit anderen Deepkit-Broker-Servern zu verbinden. Er ist für den Einsatz in einer Microservice-Architektur konzipiert, kann aber auch in einer Monolith-Anwendung verwendet werden. + +Der Client verwendet das Adapter-Pattern, um verschiedene Backends zu unterstützen. Sie können denselben Code mit unterschiedlichen Backends verwenden oder sogar mehrere Backends gleichzeitig nutzen. + +Aktuell sind 3 Adapter verfügbar. Einer ist der Standard-Adapter `BrokerDeepkitAdapter`, der mit dem Deepkit-Broker-Server kommuniziert und mit Deepkit Broker out of the box mitgeliefert wird (inklusive Server). +Der zweite ist `BrokerRedisAdapter` in [@deepkit/broker-redis](./package/broker-redis), der mit einem Redis-Server kommuniziert. Der dritte ist `BrokerMemoryAdapter`, ein In-Memory-Adapter für Testzwecke. + +## Installation + +Deepkit Broker wird standardmäßig installiert und aktiviert, wenn das [Deepkit Framework](./framework.md) verwendet wird. Andernfalls können Sie es über Folgendes installieren: + +```bash +npm install @deepkit/broker +``` + +## Broker-Klassen + +Deepkit Broker stellt diese Haupt-Broker-Klassen bereit: + +- **BrokerCache** - L2-Cache-Abstraktion mit Cache-Invalidierung +- **BrokerBus** - Message Bus (Pub/Sub) +- **BrokerQueue** - Queue-System +- **BrokerLock** - Verteilter Lock +- **BrokerKeyValue** - Key/Value Store + +Diese Klassen sind dafür ausgelegt, einen Broker-Adapter zu verwenden, um typsicher mit einem Broker-Server zu kommunizieren. + +```typescript +import { BrokerBus, BrokerMemoryAdapter } from '@deepkit/broker'; + +const bus = new BrokerBus(new BrokerMemoryAdapter()); +const channel = bus.channel<{ foo: string }>('my-channel-name'); +await channel.subscribe((message) => { + console.log('received message', message); +}); + +await channel.publish({ foo: 'bar' }); +``` + +Das FrameworkModule stellt den Standard-Adapter bereit, registriert ihn und verbindet sich mit dem Deepkit-Broker-Server, der standardmäßig im selben Prozess läuft. + +Dank Runtime Types und des Aufrufs `channel('my-channel-name');` ist alles typsicher und die Nachricht kann direkt im Adapter automatisch validiert und serialisiert werden. +Die Standardimplementierung `BrokerDeepkitAdapter` übernimmt das automatisch für Sie (und verwendet BSON für die Serialisierung). + +Beachten Sie, dass jede Broker-Klasse ihr eigenes Adapter-Interface hat, sodass Sie nur die Methoden implementieren können, die Sie benötigen. Der `BrokerDeepkitAdapter` implementiert all diese Interfaces und kann in allen Broker-APIs verwendet werden. + +## Anwendungsintegration + +Um diese Klassen in einer Deepkit-Anwendung mit Dependency Injection zu verwenden, können Sie das `FrameworkModule` nutzen, das Folgendes bereitstellt: + +- einen Standard-Adapter für den Broker-Server +- den Broker-Server selbst (und startet ihn automatisch) +- und registriert alle Broker-Klassen als Provider + +Das `FrameworkModule` stellt basierend auf der Konfiguration einen Standard-Broker-Adapter für den konfigurierten Broker-Server bereit. +Es registriert außerdem Provider für alle Broker-Klassen, sodass Sie diese (z. B. BrokerBus) direkt in Ihre Services und Provider-Factories injizieren können. + +```typescript +// in einer separaten Datei, z. B. broker-channels.ts +type MyBusChannel = BrokerBusChannel; + +const app = new App({ + providers: [ + Service, + provide((bus: BrokerBus) => bus.channel('my-channel-name')), + ], + imports: [new FrameworkModule({ + broker: { + // Wenn startOnBootstrap true ist, startet der Broker-Server unter dieser Adresse. Unix-Socket-Pfad oder Host:Port-Kombination + listen: 'localhost:8811', // oder 'var/broker.sock'; + // Wenn ein anderer Broker-Server verwendet werden soll, ist dies seine Adresse. Unix-Socket-Pfad oder Host:Port-Kombination. + host: 'localhost:8811', //oder 'var/broker.sock'; + // Startet automatisch einen einzelnen Broker im Hauptprozess. Deaktivieren Sie dies, wenn Sie einen eigenen Broker-Node haben. + startOnBootstrap: true, + }, + })], +}); +``` + +Sie können dann die abgeleiteten Broker-Klassen (oder die Broker-Klasse direkt) in Ihre Services injizieren: + +```typescript +import { MyBusChannel } from './broker-channels.ts'; + +class Service { + constructor(private bus: MyBusChannel) { + } + + async addUser() { + await this.bus.publish({ foo: 'bar' }); + } +} +``` + +Es ist immer eine gute Idee, einen abgeleiteten Typ für Ihre Channels zu erstellen (wie oben mit `MyBusChannel`), sodass Sie diese einfach in Ihre Services injizieren können. +Andernfalls müssten Sie `BrokerBus` injizieren und jedes Mal `channel('my-channel-name')` aufrufen, was fehleranfällig ist und nicht DRY. + +Fast alle Broker-Klassen haben diese Art von Ableitung, sodass Sie sie an einer Stelle definieren und überall verwenden können. Siehe die entsprechende Broker-Klassen-Dokumentation für weitere Informationen. + +## Benutzerdefinierter Adapter + +Wenn Sie einen benutzerdefinierten Adapter benötigen, können Sie Ihren eigenen Adapter erstellen, indem Sie eines oder mehrere der folgenden Interfaces aus `@deepkit/broker` implementieren: + +```typescript +export type BrokerAdapter = BrokerAdapterCache & BrokerAdapterBus & BrokerAdapterLock & BrokerAdapterQueue & BrokerAdapterKeyValue; +``` + +```typescript +import { BrokerAdapterBus, BrokerBus, Release } from '@deepkit/broker'; +import { Type } from '@deepkit/type'; + +class MyAdapter implements BrokerAdapterBus { + disconnect(): Promise { + // implementieren: Verbindung vom Broker-Server trennen + } + async publish(name: string, message: any, type: Type): Promise { + // implementieren: Nachricht an den Broker-Server senden, name ist 'my-channel-name', message ist { foo: 'bar' } + } + async subscribe(name: string, callback: (message: any) => void, type: Type): Promise { + // implementieren: Beim Broker-Server subscriben, name ist 'my-channel-name' + } +} + +// oder BrokerDeepkitAdapter für den Standard-Adapter +const adapter = new MyAdapter; +const bus = new BrokerBus(adapter); + +``` + +## Broker-Server + +Der Broker-Server startet standardmäßig automatisch, sobald Sie das `FrameworkModule` importieren und den Befehl `server:start` ausführen. +Alle Broker-Klassen sind standardmäßig so konfiguriert, dass sie sich mit diesem Server verbinden. + +In einer Produktionsumgebung würden Sie den Broker-Server in einem separaten Prozess oder auf einer anderen Maschine ausführen. +Anstatt `server:start` zu verwenden, würden Sie den Broker-Server mit `server:broker:start` starten. + +Wenn Sie viel Traffic haben und Skalierbarkeit benötigen, sollten Sie stattdessen den [Redis-Adapter](./package/broker-redis.md) verwenden. Er ist in der Einrichtung etwas komplizierter, +da Sie einen Redis-Server betreiben müssen, aber er ist performanter und kann mehr Traffic verarbeiten. + +```bash +ts-node app.ts server:broker:start +``` + +Dies startet den Server auf dem Host, der z. B. über `new FrameworkModule({broker: {listen: 'localhost:8811'}})` konfiguriert ist. +Sie können die Adresse auch über Umgebungsvariablen ändern, wenn Sie `app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper'});` aktiviert haben: + +```bash +APP_FRAMEWORK_BROKER_LISTEN=localhost:8811 ts-node app.ts server:broker:start +``` + +Wenn Sie den Server manuell starten, stellen Sie sicher, dass Sie den automatischen Start des Broker-Servers über `startOnBootstrap: false` in Ihrer Anwendungskonfiguration deaktivieren. \ No newline at end of file diff --git a/website/src/translations/de/documentation/broker/atomic-locks.md b/website/src/translations/de/documentation/broker/atomic-locks.md new file mode 100644 index 000000000..935e24275 --- /dev/null +++ b/website/src/translations/de/documentation/broker/atomic-locks.md @@ -0,0 +1,75 @@ +# Broker Atomare Locks + +Deepkit Broker Locks ist eine einfache Möglichkeit, atomare Locks über mehrere Prozesse oder Maschinen hinweg zu erstellen. + +Es ist eine einfache Möglichkeit sicherzustellen, dass jeweils nur ein Prozess einen bestimmten Code-Block ausführen kann. + +## Verwendung + +```typescript +import { BrokerLock } from '@deepkit/broker'; + +const lock = new BrokerLock(adapter); + +// Lock ist für 60 Sekunden aktiv. +// Acquisition-Timeout beträgt 10 Sekunden. +const myLock = lock.item('my-lock', { ttl: '60s', timeout: '10s' }); + +async function criticalSection() { + // Hält den Lock, bis die Funktion beendet ist. + // Bereinigt den Lock automatisch, selbst wenn die Funktion einen Error wirft. + await using hold = await lock1.hold(); +} +``` + +Der Lock unterstützt [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management), was bedeutet, dass du keinen try-catch-Block verwenden musst, um sicherzustellen, dass der Lock ordnungsgemäß freigegeben wird. + +Um einen Lock manuell zu erwerben und freizugeben, kannst du die Methoden `acquire` und `release` verwenden. + +```typescript +// Dies blockiert, bis der Lock erworben wurde. +// oder wirft, wenn das Timeout erreicht ist +await myLock.acquire(); + +try { + // etwas Kritisches ausführen +} finally { + await myLock.release(); +} +``` + +## Verwendung in der App + +Ein vollständiges Beispiel dafür, wie du BrokerLock in deiner Anwendung verwendest. +Die Class ist im Dependency-Injection-Container automatisch verfügbar, wenn du das `FrameworkModule` importierst. +Siehe die Seite Erste Schritte für weitere Informationen. + +```typescript +import { BrokerLock, BrokerLockItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// Verschiebe diesen Type in eine gemeinsame Datei +type MyCriticalLock = BrokerLockItem; + +class Service { + constructor(private criticalLock: MyCriticalLock) { + } + + async doSomethingCritical() { + await using hold = await this.criticalLock.hold(); + + // etwas Kritisches ausführen, + // Lock wird automatisch freigegeben + } +} + +const app = new App({ + providers: [ + Service, + provide((lock: BrokerLock) => lock.item('my-critical-lock', { ttl: '60s', timeout: '10s' })), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/broker/cache.md b/website/src/translations/de/documentation/broker/cache.md new file mode 100644 index 000000000..de6575cea --- /dev/null +++ b/website/src/translations/de/documentation/broker/cache.md @@ -0,0 +1,91 @@ +# Broker Cache + +Die Deepkit Broker Cache Class ist ein mehrstufiger Cache (2 Ebenen), der einen flüchtigen lokalen Cache im Speicher hält und Daten vom Broker-Server nur dann abruft, wenn die Daten nicht im Cache sind, stale sind oder invalidiert wurden. Das ermöglicht sehr hohe Performance und geringe Latenz beim Datenabruf. + +Der Cache ist type-safe konzipiert und serialisiert und deserialisiert Daten automatisch (mittels BSON). Er unterstützt zudem Cache Invalidation und Cache Clearing. Die Implementation stellt sicher, dass pro Prozess der Cache nur einmal neu aufgebaut wird, selbst wenn mehrere Requests gleichzeitig auf dasselbe Cache Item zugreifen. + +Die Daten werden nicht auf dem Server persistiert, sondern nur im Speicher gehalten. Wenn der Server neu startet, gehen alle Daten verloren. + +## Verwendung + +Stellen Sie sicher, die Getting Started-Seite zu lesen, um zu lernen, wie Sie Ihre Anwendung korrekt einrichten, sodass BrokerCache im Dependency Injection Container verfügbar ist. + +Die Cache Abstraction im Deepkit Broker unterscheidet sich stark von einem einfachen Key/Value Store. Sie funktioniert, indem man einen Cache-Namen und eine Builder Function definiert, die automatisch aufgerufen wird, wenn der Cache leer oder stale ist. Diese Builder Function ist dafür verantwortlich, die Daten zu erstellen, die anschließend im Cache gespeichert werden. + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; + +const cache = new BrokerCache(adapter); + +const cacheItem = cache.item('my-cache', async () => { + // Dies ist die Builder Function, die aufgerufen wird, wenn + // der Cache leer oder stale ist + return 'hello world'; +}); + + +// Prüfen, ob der Cache stale oder leer ist +await cacheItem.exists(); + +// Daten aus dem Cache holen oder vom Broker-Server abrufen +// Ist der Cache leer oder stale, wird die Builder Function aufgerufen +// und das Ergebnis zurückgegeben und an den Broker-Server gesendet. +const topUsers = await cacheItem.get(); + +// Den Cache invalidieren, sodass der nächste get()-Aufruf +// die Builder Function erneut aufruft. +// Löscht den lokalen Cache und den Server-Cache. +await cacheItem.invalidate(); + +// Daten manuell im Cache setzen +await cacheItem.set(xy); +``` + +## App-Verwendung + +Ein vollständiges Beispiel, wie Sie den BrokerCache in Ihrer Anwendung verwenden. +Die Class ist automatisch im Dependency Injection Container verfügbar, wenn Sie das `FrameworkModule` importieren. +Siehe die Getting Started-Seite für weitere Informationen. + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// Es ist sinnvoll, diese Types in einer gemeinsamen Datei zu definieren, damit Sie sie wiederverwenden +// und in Ihre Services injizieren können +type MyCacheItem = BrokerCacheItem; + +function createMyCache(cache: BrokerCache, database: Database) { + return cache.item('top-users', async () => { + // Dies ist die Builder Function, die aufgerufen wird, wenn + // der Cache leer oder stale ist + return await database.query(User) + .limit(10).orderBy('score').find(); + }); +} + +class Service { + constructor(private cacheItem: MyCacheItem) { + } + + async getTopUsers() { + return await this.cacheItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + Database, + provide(createMyCache), + ], + imports: [ + new FrameworkModule(), + ], +}); + +const cacheItem = app.get(); + +// Daten aus dem Cache holen oder vom Broker-Server abrufen +const topUsers = await cacheItem.get(); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/broker/key-value.md b/website/src/translations/de/documentation/broker/key-value.md new file mode 100644 index 000000000..598f1d76c --- /dev/null +++ b/website/src/translations/de/documentation/broker/key-value.md @@ -0,0 +1,90 @@ +# Broker Key-Value + +Die Deepkit Broker Key-Value Class ist eine einfache Key/Value-Store-Abstraktion, die mit dem Broker-Server funktioniert. Sie ist eine einfache Möglichkeit, Daten vom Broker-Server zu speichern und abzurufen. + +Es ist kein lokales Caching implementiert. Alle `get`-Aufrufe sind jedes Mal echte Netzwerk-Requests an den Broker-Server. Um dies zu vermeiden, verwenden Sie die Broker Cache Abstraktion. + +Die Daten werden auf dem Server nicht persistent gespeichert, sondern nur im Speicher gehalten. Wenn der Server neu startet, gehen alle Daten verloren. + +## Verwendung + +```typescript +import { BrokerKeyValue } from '@deepkit/broker'; + +const keyValue = new BrokerKeyValue(adapter, { + ttl: '60s', // Time to Live für jeden Key. 0 bedeutet keine TTL (Standard). +}); + +const item = keyValue.item('key1'); + +await item.set(123); +console.log(await item.get()); //123 + +await item.remove(); +``` + +Die Daten werden basierend auf dem gegebenen Type automatisch mit BSON serialisiert und deserialisiert. + +Die Methods `set` und `get` können auch direkt auf der `BrokerKeyValue`-Instanz aufgerufen werden, haben jedoch den Nachteil, dass Sie jedes Mal den Key und den Type übergeben müssen. + +```typescript +await keyValue.set('key1', 123); +console.log(await keyValue.get('key1')); //123 +``` + +## Increment + +Die Method `increment` ermöglicht es, den Wert eines Keys atomar um einen gegebenen Betrag zu erhöhen. + +Beachten Sie, dass dies einen eigenen Eintrag im Speicher auf dem Server erstellt und nicht mit `set` oder `get` kompatibel ist. + +```typescript + +const activeUsers = keyValue.item('activeUsers'); + +// Atomar um 1 inkrementieren +await activeUsers.increment(1); + +await activeUsers.increment(-1); + +// Die einzige Möglichkeit, den aktuellen Wert zu erhalten, ist, increment mit 0 aufzurufen +const current = await activeUsers.increment(0); + +// entfernt den Eintrag +await activeUsers.remove(); +``` + +## App-Verwendung + +Ein vollständiges Beispiel, wie Sie BrokerKeyValue in Ihrer Anwendung verwenden. +Die Class ist im Dependency-Injection-Container automatisch verfügbar, wenn Sie das `FrameworkModule` importieren. +Weitere Informationen finden Sie auf der Seite Erste Schritte. + +```typescript +import { BrokerKeyValue, BrokerKeyValueItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// Verschieben Sie diesen Type in eine gemeinsame Datei +type MyKeyValueItem = BrokerKeyValueItem; + +class Service { + constructor(private keyValueItem: MyKeyValueItem) { + } + + async getTopUsers(): Promise { + // Könnte undefined sein. Sie müssen diesen Fall behandeln. + // Verwenden Sie Broker Cache, wenn Sie dies vermeiden möchten. + return await this.keyValueItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + provide((keyValue: BrokerKeyValue) => keyValue.item('top-users')), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/broker/message-bus.md b/website/src/translations/de/documentation/broker/message-bus.md new file mode 100644 index 000000000..cf526b978 --- /dev/null +++ b/website/src/translations/de/documentation/broker/message-bus.md @@ -0,0 +1,137 @@ +# Broker Bus + +Deepkit Message Bus ist ein Message-Bus-System (Pub/Sub, verteiltes Event-System), das es dir ermöglicht, Nachrichten oder Events zwischen verschiedenen Teilen deiner Anwendung zu senden. + +Es kann in Microservices, Monolithen oder jeder anderen Art von Anwendung verwendet werden. Perfekt geeignet für ereignisgesteuerte Architekturen. + +Es unterscheidet sich vom Deepkit Event-System, das für In-Process-Events verwendet wird. Der Broker Bus wird für Events genutzt, die an andere Prozesse oder Server gesendet werden müssen. Broker Bus ist auch perfekt geeignet, wenn du zwischen mehreren Workern kommunizieren möchtest, die automatisch vom FrameworkModule gestartet wurden, z. B. `new FrameworkModule({workers: 4})`. + +Das System ist darauf ausgelegt, type-safe zu sein und serialisiert sowie deserialisiert Nachrichten automatisch (mit BSON). Wenn du [Validierung](../runtime-types/validation.md) zu deinen Nachrichtentypen hinzufügst, werden die Nachrichten außerdem vor dem Senden und nach dem Empfangen validiert. Das stellt sicher, dass die Nachrichten immer im korrekten Format sind und die erwarteten Daten enthalten. + +## Verwendung + +```typescript +import { BrokerBus } from '@deepkit/broker'; + +const bus = new BrokerBus(adapter); + +// verschiebe diesen Typ in eine gemeinsame Datei +type UserEvent = { type: 'user-created', id: number } | { type: 'user-deleted', id: number }; + +const channel = bus.channel('user-events'); + +await channel.subscribe((event) => { + if (event.type === 'user-created') { + console.log('User created', event.id); + } else if (event.type === 'user-deleted') { + console.log('User deleted', event.id); + } +}); + +await channel.publish({ type: 'user-created', id: 1 }); +``` + +Wenn du einen Namen und einen Typ für den Channel definierst, stellst du sicher, dass nur Nachrichten des korrekten Typs gesendet und empfangen werden. +Die Daten werden automatisch serialisiert und deserialisiert (mit BSON). + +## App-Verwendung + +Ein vollständiges Beispiel dafür, wie du den BrokerBus in deiner Anwendung verwendest. +Die Klasse ist automatisch im Dependency-Injection-Container verfügbar, wenn du das `FrameworkModule` importierst. +Siehe die Getting-Started-Seite für weitere Informationen. + +### Subject + +Der Standardansatz zum Senden und Empfangen von Nachrichten ist die Verwendung des rxjs-`Subject`-Typs. Seine `subscribe`- und `next`-Methoden ermöglichen es dir, Nachrichten type-safe zu senden und zu empfangen. Alle Subject-Instanzen werden vom Broker verwaltet, und sobald das Subject vom Garbage Collector eingesammelt wurde, wird die Subscription aus dem Broker-Backend (z. B. Redis) entfernt. + +Überschreibe den `BusBrokerErrorHandler` von BrokerBus, um Fehler beim Publishen oder Subscriben von Nachrichten zu behandeln. + +Dieser Ansatz entkoppelt deinen Business-Code sauber vom Broker-Server und ermöglicht dir, denselben Code in einer Testumgebung ohne Broker-Server zu verwenden. + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusSubject } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; +import { Subject } from 'rxjs'; + +// verschiebe diesen Typ in eine gemeinsame Datei +type MyChannel = Subject<{ + id: number; + name: string; +}>; + +class Service { + // MyChannel ist kein Singleton, sondern für jede Request wird eine neue Instanz erstellt. + // Seine Lebenszeit wird vom Framework überwacht und sobald das Subject vom Garbage Collector + // eingesammelt wurde, wird die Subscription aus dem Broker-Backend (z. B. Redis) entfernt. + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }); + } + + update() { + this.channel.next({ id: 1, name: 'Peter' }); + } +} + +@rpc.controller('my-controller') +class MyRpcController { + constructor(private channel: MyChannel) { + } + + @rpc.action() + getChannelData(): MyChannel { + return this.channel; + } +} + +const app = new App({ + controllers: [MyRpcController], + providers: [ + Service, + provideBusSubject('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` + +### BusChannel + +Wenn du eine Bestätigung für das Senden der Nachricht benötigst und Fehler in jedem Fall behandeln möchtest, kannst du den `BrokerBusChannel`-Typ verwenden. Dessen `subscribe`- und `publish`-Methoden geben jeweils eine Promise zurück. + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusChannel } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// verschiebe diesen Typ in eine gemeinsame Datei +type MyChannel = BrokerBusChannel<{ + id: number; + name: string; +}>; + +class Service { + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }).catch(e => { + console.error('Error while subscribing', e); + }); + } + + async update() { + await this.channel.publish({ id: 1, name: 'Peter' }); + } +} + +const app = new App({ + providers: [ + Service, + provideBusChannel('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/broker/message-queue.md b/website/src/translations/de/documentation/broker/message-queue.md new file mode 100644 index 000000000..2909c2c29 --- /dev/null +++ b/website/src/translations/de/documentation/broker/message-queue.md @@ -0,0 +1,116 @@ +# Broker-Queue + +Deepkit Message Queue ist ein Message-Queue-System, das es ermöglicht, Messages an den Queue-Server zu senden und von Workern verarbeiten zu lassen. + +Das System ist type-safe konzipiert und serialisiert sowie deserialisiert Messages automatisch (mit BSON). + +Die Daten werden auf dem Server persistiert, sodass sie selbst bei einem Serverabsturz nicht verloren gehen. + +## Verwendung + +```typescript +import { BrokerQueue, BrokerQueueChannel } from '@deepkit/broker'; + +const queue = new BrokerQueue(adapter); + +type User = { id: number, username: string }; + +const registrationChannel = queue.channel('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +}); + +// Ein Worker konsumiert die Messages. +// Dies geschieht üblicherweise in einem separaten Prozess. +await registrationChannel.consume(async (user) => { + console.log('User registered', user); + // Wenn der Worker hier abstürzt, geht die Message nicht verloren. + // Sie wird automatisch an einen anderen Worker erneut zugestellt. + // Wenn dieser Callback ohne Error zurückkehrt, wird die Message + // als verarbeitet markiert und schließlich entfernt. +}); + +// Die Anwendung, die die Message sendet +await registrationChannel.produce({ id: 1, username: 'Peter' }); +``` + +## App-Verwendung + +Ein vollständiges Beispiel, wie Sie die BrokerQueue in Ihrer Anwendung verwenden. +Die Class ist im Dependency Injection Container automatisch verfügbar, wenn Sie das `FrameworkModule` importieren. +Siehe die Seite Erste Schritte für weitere Informationen. + +Um die Queuesysteme optimal zu nutzen, wird empfohlen, mehrere Worker zu starten, die die Messages konsumieren. +Dafür schreiben Sie eine separate `App`, die sich von der Hauptanwendung unterscheidet, die ggf. HTTP-Routen usw. hat. + +Gemeinsame Services teilen Sie über ein gemeinsames App-Modul. Channel-Definitionen werden über den Rest der Anwendung in einer gemeinsamen Datei geteilt. + +```typescript +// Datei: channels.ts + +export type RegistrationChannel = BrokerQueueChannel; +export const registrationChannelProvider = provide((queue: BrokerQueue) => queue.channel('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +})); +``` + +```typescript +// Datei: worker.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +async function consumerCommand( + channel: RegistrationChannel, + database: Database) { + + await channel.consume(async (user) => { + // Etwas mit dem User tun, + // vielleicht Informationen speichern, E-Mails senden, etc. + }); + + // Die Verbindung zum Broker hält den Prozess am Leben. +} + +const app = new App({ + providers: [ + Database, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +app.command('consumer', consumerCommand); + +// Startet den obigen Worker-Command direkt +void app.run('consumer'); +``` + +Und in Ihrer Anwendung produzieren Sie Messages wie folgt: + +```typescript +// Datei: app.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +class Service { + constructor(private channel: RegistrationChannel) { + } + + async registerUser(user: User) { + await this.channel.produce(user); + } +} + +const app = new App({ + providers: [ + Service, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +void app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/dependency-injection.md b/website/src/translations/de/documentation/dependency-injection.md new file mode 100644 index 000000000..29c43030f --- /dev/null +++ b/website/src/translations/de/documentation/dependency-injection.md @@ -0,0 +1,197 @@ +# Dependency Injection + +Dependency Injection (DI) ist ein Design Pattern, bei dem Klassen und Funktionen ihre Dependencies _empfangen_. Es folgt dem Prinzip der Inversion of Control (IoC) und hilft, komplexen Code besser zu trennen, um Testbarkeit, Modularität und Übersichtlichkeit deutlich zu verbessern. Obwohl es andere Design Patterns gibt, wie etwa das Service Locator Pattern, um das IoC-Prinzip anzuwenden, hat sich DI insbesondere in Enterprise-Software als dominantes Pattern etabliert. + +Um das Prinzip von IoC zu veranschaulichen, hier ein Beispiel: + +```typescript +import { HttpClient } from 'http-library'; + +class UserRepository { + async getUsers(): Promise { + const client = new HttpClient(); + return await client.get('/users'); + } +} +``` + +Die Klasse UserRepository hat einen HttpClient als Dependency. Diese Dependency ist an sich nichts Besonderes, problematisch ist jedoch, dass `UserRepository` den HttpClient selbst erzeugt. +Es scheint eine gute Idee zu sein, die Erstellung des HttpClient in der UserRepository zu kapseln, aber das ist nicht der Fall. Was, wenn wir den HttpClient austauschen wollen? Was, wenn wir UserRepository in einem Unit Test testen wollen, ohne echte HTTP-Requests rauszulassen? Woher wissen wir, dass die Klasse überhaupt einen HttpClient verwendet? + +## Inversion of Control + +Im Sinne der Inversion of Control (IoC) gibt es folgende alternative Variante, die den HttpClient als explizite Dependency im Konstruktor festlegt (auch bekannt als Constructor Injection). + +```typescript +class UserRepository { + constructor( + private http: HttpClient + ) {} + + async getUsers(): Promise { + return await this.http.get('/users'); + } +} +``` + +Nun ist nicht mehr UserRepository für die Erstellung des HttpClient zuständig, sondern der Nutzer von UserRepository. Das ist Inversion of Control (IoC). Die Kontrolle wurde umgekehrt bzw. invertiert. Konkret wendet dieser Code Dependency Injection an, weil Dependencies empfangen (injiziert) und nicht mehr erstellt oder angefordert werden. Dependency Injection ist nur eine Variante von IoC. + +## Service Locator + +Neben DI ist Service Locator (SL) ebenfalls eine Möglichkeit, das IoC-Prinzip anzuwenden. Dies gilt gemeinhin als Gegenstück zu Dependency Injection, da Dependencies angefordert statt empfangen werden. Würde HttpClient im obigen Code wie folgt angefordert, spräche man vom Service Locator Pattern. + +```typescript +class UserRepository { + async getUsers(): Promise { + const client = locator.getHttpClient(); + return await client.get('/users'); + } +} +``` + +Die Function `locator.getHttpClient` kann jeden beliebigen Namen haben. Alternativen wären Funktionsaufrufe wie `useContext(HttpClient)`, `getHttpClient()`, `await import("client")`, oder Container-Abfragen wie `container.get(HttpClient)` oder `container.http`. Ein Import eines Globals ist eine leicht andere Variante eines Service Locators, bei der das Modulsystem selbst als Locator dient: + +```typescript +import { httpClient } from 'clients' + +class UserRepository { + async getUsers(): Promise { + return await httpClient.get('/users'); + } +} +``` + +Allen diesen Varianten ist gemeinsam, dass sie die HttpClient-Dependency explizit anfordern und der Code sich bewusst ist, dass es einen Service-Container gibt. Das koppelt den Code stark an das Framework und ist etwas, das man vermeiden möchte, um den Code sauber zu halten. + +Die Service-Anforderung kann nicht nur an Properties als Default-Wert erfolgen, sondern auch irgendwo mitten im Code. Da „mitten im Code“ bedeutet, dass es nicht Teil eines Type-Interfaces ist, bleibt die Verwendung des HttpClient verborgen. Je nach Variante, wie der HttpClient angefordert wird, kann es mitunter sehr schwierig oder völlig unmöglich sein, ihn durch eine andere Implementation zu ersetzen. Gerade im Bereich von Unit Tests und der Übersichtlichkeit können hier Schwierigkeiten entstehen, sodass der Service Locator in bestimmten Situationen als Anti-Pattern eingestuft wird. + +## Dependency Injection + +Bei Dependency Injection wird nichts angefordert, sondern es wird vom Nutzer explizit bereitgestellt oder vom Code empfangen. Der Consumer hat keinen Zugriff auf einen Service-Container und weiß nicht, wie `HttpClient` erstellt oder bezogen wird. Im Kern ermöglicht es, den Code vom IoC-Framework zu entkoppeln und damit sauberer zu halten. + +Es wird lediglich deklariert, dass ein `HttpClient` als Typ benötigt wird. Ein entscheidender Unterschied und Vorteil von Dependency Injection gegenüber Service Locator ist, dass Code, der Dependency Injection verwendet, auch ohne jeglichen Service-Container und ohne Service-Identifikationssystem einwandfrei funktioniert (du musst deinem Service keinen Namen geben). Es ist einfach eine Typdeklaration, die auch außerhalb des IoC-Framework-Kontexts funktioniert. + +Wie im früheren Beispiel zu sehen, wurde das Dependency Injection Pattern dort bereits angewendet. Konkret ist dort Constructor Injection zu sehen, da die Dependency im Konstruktor deklariert ist. UserRepository muss nun wie folgt instanziert werden. + +```typescript +const users = new UserRepository(new HttpClient()); +``` + +Der Code, der UserRepository verwenden möchte, muss auch alle seine Dependencies bereitstellen (injizieren). Ob HttpClient jedes Mal erzeugt werden soll oder jedes Mal derselbe verwendet wird, entscheidet nun der Nutzer der Klasse und nicht mehr die Klasse selbst. Er wird nicht mehr angefordert (aus Sicht der Klasse) wie beim Service Locator oder vollständig selbst erstellt wie im anfänglichen Beispiel. Diese Umkehrung des Flusses hat verschiedene Vorteile: + +* Der Code ist leichter verständlich, da alle Dependencies explizit sichtbar sind. +* Der Code ist leichter zu testen, da alle Dependencies eindeutig und bei Bedarf leicht veränderbar sind. +* Der Code ist modularer, da Dependencies leicht austauschbar sind. +* Es fördert das „Separation of Concerns“-Prinzip, da UserRepository nicht mehr selbst für die Erstellung sehr komplexer Dependencies verantwortlich ist. + +Ein offensichtlicher Nachteil zeigt sich jedoch direkt: Muss ich wirklich alle Dependencies wie den HttpClient selbst erstellen oder verwalten? Ja und nein. Ja, es gibt viele Fälle, in denen es völlig legitim ist, die Dependencies selbst zu verwalten. Das Kennzeichen einer guten API ist, dass Dependencies nicht aus dem Ruder laufen und sich selbst dann angenehm nutzen lassen. Für viele Anwendungen oder komplexe Bibliotheken kann das durchaus zutreffen. Um eine sehr komplexe Low-Level-API mit vielen Dependencies dem Nutzer vereinfacht bereitzustellen, eignet sich das Facade Pattern hervorragend. + +## Dependency Injection Container + +Für komplexere Anwendungen ist es jedoch nicht nötig, alle Dependencies selbst zu verwalten, denn genau dafür ist ein sogenannter Dependency Injection Container da. Dieser erstellt nicht nur alle Objekte automatisch, sondern „injiziert“ die Dependencies ebenfalls automatisch, sodass ein manueller „new“-Aufruf nicht mehr notwendig ist. Es gibt verschiedene Arten der Injection, wie Constructor Injection, Method Injection oder Property Injection. So lassen sich auch komplizierte Architekturen mit vielen Dependencies leicht verwalten. + +Einen Dependency Injection Container bringt Deepkit in `@deepkit/injector` mit oder bereits fertig integriert über App-Module im Deepkit Framework. Der obige Code sähe unter Verwendung einer Low-Level-API aus dem `@deepkit/injector`-Package so aus: + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders( + [UserRepository, HttpClient] +); + +const userRepo = injector.get(UserRepository); + +const users = await userRepo.getUsers(); +``` + +Das `injector`-Objekt ist in diesem Fall der Dependency Injection Container. Anstatt „new UserRepository“ zu verwenden, gibt der Container mittels `get(UserRepository)` eine Instanz von UserRepository zurück. Um den Container statisch zu initialisieren, wird der Function `InjectorContext.forProviders` eine Liste von Providern übergeben (in diesem Fall einfach die Klassen). +Da es bei DI darum geht, Dependencies bereitzustellen, wird der Container mit den Dependencies versorgt, daher der Fachbegriff „Provider“. + +Es gibt mehrere Arten von Providern: ClassProvider, ValueProvider, ExistingProvider, FactoryProvider. Zusammen ermöglichen sie es, sehr flexible Architekturen mit einem DI-Container abzubilden. + +Alle Dependencies zwischen Providern werden automatisch aufgelöst und sobald ein `injector.get()`-Aufruf erfolgt, werden die Objekte und Dependencies erstellt, gecached und korrekt entweder als Konstruktor-Argument (Constructor Injection), als Property gesetzt (Property Injection) oder an einen Methodenaufruf übergeben (Method Injection). + +Um nun den HttpClient gegen einen anderen auszutauschen, kann ein weiterer Provider (hier der ValueProvider) für HttpClient definiert werden: + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useValue: new AnotherHttpClient()}, +]); +``` + +Sobald UserRepository über `injector.get(UserRepository)` angefordert wird, erhält es das AnotherHttpClient-Objekt. Alternativ kann hier sehr gut ein ClassProvider verwendet werden, sodass auch alle Dependencies von AnotherHttpClient vom DI-Container verwaltet werden. + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useClass: AnotherHttpClient}, +]); +``` + +Alle Arten von Providern sind im Abschnitt [Dependency Injection Providers](./dependency-injection/providers.md) aufgelistet und erklärt. + +Es sei hier erwähnt, dass Deepkits DI-Container nur mit Deepkits Runtime Types funktioniert. Das bedeutet, dass jeder Code, der Klassen, Types, Interfaces und Functions enthält, vom Deepkit Type Compiler kompiliert werden muss, um die Typinformationen zur Laufzeit verfügbar zu haben. Siehe das Kapitel [Runtime Types](./runtime-types.md). + +## Dependency Inversion + +Das Beispiel von UserRepository oben zeigt, dass UserRepository von einer Low-Level-HTTP-Library abhängt. Zudem wird eine konkrete Implementation (Class) statt einer Abstraktion (Interface) als Dependency deklariert. Auf den ersten Blick scheint dies den objektorientierten Paradigmen zu entsprechen, kann aber insbesondere in komplexen und großen Architekturen zu Problemen führen. + +Eine alternative Variante wäre, die HttpClient-Dependency in eine Abstraktion (Interface) zu überführen und damit keinen Code aus einer HTTP-Library in UserRepository zu importieren. + +```typescript +interface HttpClientInterface { + get(path: string): Promise; +} + +class UserRepository { + concstructor( + private http: HttpClientInterface + ) {} + + async getUsers(): Promise { + return await this.http.get('/users'); + } +} +``` + +Dies nennt man das Dependency Inversion Principle. UserRepository hat nun keine direkte Dependency mehr auf eine HTTP-Library und basiert stattdessen auf einer Abstraktion (Interface). Es erfüllt damit zwei grundlegende Ziele dieses Prinzips: + +* High-Level-Module sollten nichts aus Low-Level-Modulen importieren. +* Implementationen sollten auf Abstraktionen (Interfaces) basieren. + +Das Zusammenführen der beiden Implementationen (UserRepository mit einer HTTP-Library) kann nun über den DI-Container erfolgen. + +```typescript +import { InjectorContext } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); +``` + +Da der DI-Container von Deepkit in der Lage ist, abstrakte Dependencies (Interfaces) wie diese von HttpClientInterface aufzulösen, erhält UserRepository automatisch die Implementation von HttpClient, da HttpClient das Interface HttpClientInterface implementiert hat. + +Dies geschieht entweder dadurch, dass HttpClient explizit HttpClientInterface implementiert (`class HttpClient implements HttpClientInterface`), oder dadurch, dass die API von HttpClient einfach mit HttpClientInterface kompatibel ist. + +Sobald HttpClient seine API verändert (zum Beispiel die `get`-Methode entfernt) und damit nicht mehr mit HttpClientInterface kompatibel ist, wirft der DI-Container einen Error ("the HttpClientInterface dependency was not provided"). Hier ist der Nutzer, der beide Implementationen zusammenbringen möchte, in der Pflicht, eine Lösung zu finden. Als Beispiel könnte hier eine Adapter-Klasse registriert werden, die HttpClientInterface implementiert und die Methodenaufrufe korrekt an HttpClient weiterleitet. + +Alternativ kann das HttpClientInterface direkt mit einer konkreten Implementation bereitgestellt werden. + +```typescript +import { InjectorContext, provide } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository, HttpClientInterface } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + provide({useClass: HttpClient}), +]); +``` + +Es ist zu beachten, dass das Dependency Inversion Principle zwar in der Theorie seine Vorteile hat, in der Praxis jedoch auch erhebliche Nachteile mit sich bringt. Es führt nicht nur zu mehr Code (da mehr Interfaces geschrieben werden müssen), sondern auch zu mehr Komplexität (da jede Implementation nun für jede Dependency ein Interface hat). Dieser Preis lohnt sich erst, wenn die Anwendung eine gewisse Größe erreicht und diese Flexibilität benötigt wird. Wie jedes Design Pattern und Prinzip hat auch dieses seinen Kosten-Nutzen-Faktor, der durchdacht werden sollte, bevor es angewendet wird. + +Design Patterns sollten nicht blind und flächendeckend selbst für den einfachsten Code eingesetzt werden. Sind jedoch die Voraussetzungen wie eine komplexe Architektur, große Anwendungen oder ein skalierendes Team gegeben, entfalten Dependency Inversion und andere Design Patterns erst ihre wahre Stärke. \ No newline at end of file diff --git a/website/src/translations/de/documentation/dependency-injection/configuration.md b/website/src/translations/de/documentation/dependency-injection/configuration.md new file mode 100644 index 000000000..6fef850c5 --- /dev/null +++ b/website/src/translations/de/documentation/dependency-injection/configuration.md @@ -0,0 +1,92 @@ +# Konfiguration + +Der Dependency Injection Container ermöglicht ebenfalls, Konfigurationsoptionen zu injizieren. Diese Konfigurationsinjektion kann über Constructor Injection oder Property Injection empfangen werden. + +Die Module API unterstützt die Definition einer Konfigurationsdefinition, die eine reguläre Class ist. Indem man einer solchen Class Properties gibt, fungiert jede Property als Konfigurationsoption. Aufgrund der Art, wie Classes in TypeScript definiert werden können, lässt sich pro Property ein Type und Standardwerte festlegen. + +```typescript +class RootConfiguration { + domain: string = 'localhost'; + debug: boolean = false; +} + +const rootModule = new InjectorModule([UserRepository]) + .setConfigDefinition(RootConfiguration) + .addImport(lowLevelModule); +``` + +Die Konfigurationsoptionen `domain` und `debug` können nun bequem und typesafe in Providern verwendet werden. + +```typescript +class UserRepository { + constructor(private debug: RootConfiguration['debug']) {} + + getUsers() { + if (this.debug) console.debug('fetching users ...'); + } +} +``` + +Die Werte der Optionen selbst können über `configure()` gesetzt werden. + +```typescript + rootModule.configure({debug: true}); +``` + +Optionen, die keinen Standardwert haben, aber dennoch erforderlich sind, können mit einem `!` versehen werden. Das zwingt den Benutzer des Moduls, den Wert zu liefern; andernfalls tritt ein Error auf. + +```typescript +class RootConfiguration { + domain!: string; +} +``` + +## Validierung + +Außerdem können alle Serialization- und Validation-Types aus den vorherigen Kapiteln [Validierung](../runtime-types/validation.md) und [Serialisierung](../runtime-types/serialization.md) verwendet werden, um präzise festzulegen, welche Type- und Inhaltsbeschränkungen eine Option haben muss. + +```typescript +class RootConfiguration { + domain!: string & MinLength<4>; +} +``` + +## Injektion + +Konfigurationsoptionen können, wie andere Abhängigkeiten, sicher und einfach über den DI-Container injiziert werden, wie zuvor gezeigt. Die einfachste Methode ist, eine einzelne Option mithilfe des Indexzugriffsoperators zu referenzieren: + +```typescript +class WebsiteController { + constructor(private debug: RootConfiguration['debug']) {} + + home() { + if (this.debug) console.debug('visit home page'); + } +} +``` + +Konfigurationsoptionen können nicht nur einzeln, sondern auch als Gruppe referenziert werden. Hierfür wird der TypeScript Utility Type `Partial` verwendet: + +```typescript +class WebsiteController { + constructor(private options: Partial) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +Um alle Konfigurationsoptionen zu erhalten, kann auch die Konfigurations-Class direkt referenziert werden: + +```typescript +class WebsiteController { + constructor(private options: RootConfiguration) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +Es wird jedoch empfohlen, nur die Konfigurationsoptionen zu referenzieren, die tatsächlich verwendet werden. Das vereinfacht nicht nur Unit-Tests, sondern macht auch leichter ersichtlich, was der Code tatsächlich benötigt. \ No newline at end of file diff --git a/website/src/translations/de/documentation/dependency-injection/getting-started.md b/website/src/translations/de/documentation/dependency-injection/getting-started.md new file mode 100644 index 000000000..e8bc83dc7 --- /dev/null +++ b/website/src/translations/de/documentation/dependency-injection/getting-started.md @@ -0,0 +1,110 @@ +# Erste Schritte + +Da Dependency Injection in Deepkit auf Runtime Types basiert, müssen Runtime Types bereits korrekt installiert sein. Siehe [Runtime Type](../runtime-types/getting-started.md). + +Wenn dies erfolgreich erledigt ist, kann `@deepkit/injector` installiert werden oder das Deepkit Framework, das die Library bereits unter der Haube verwendet. + +```sh + npm install @deepkit/injector +``` + +Sobald die Library installiert ist, kann deren API direkt verwendet werden. + + +## Verwendung + +Um Dependency Injection zu verwenden, gibt es drei Möglichkeiten. + +* Injector-API (Low Level) +* Module-API +* App-API (Deepkit Framework) + +Soll `@deepkit/injector` ohne das Deepkit Framework verwendet werden, werden die ersten beiden Varianten empfohlen. + +### Injector-API + +Die Injector-API wurde bereits in der [Einführung zu Dependency Injection](../dependency-injection) vorgestellt. Sie zeichnet sich durch eine sehr einfache Nutzung mittels einer einzigen Class `InjectorContext` aus, die einen einzelnen DI-Container erstellt und sich besonders für einfachere Anwendungen ohne Module eignet. + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); + +const repository = injector.get(UserRepository); +``` + +Das Objekt `injector` ist in diesem Fall der Dependency-Injection-Container. Die Function `InjectorContext.forProviders` nimmt ein Array von Providern entgegen. Siehe den Abschnitt [Dependency Injection Providers](dependency-injection.md#di-providers), um zu erfahren, welche Werte übergeben werden können. + +### Module-API + +Eine komplexere API ist die Class `InjectorModule`, mit der Provider in verschiedenen Modulen abgelegt werden können, um pro Module mehrere gekapselte DI-Container zu erstellen. Außerdem ermöglicht dies die Verwendung von Configuration Classes pro Module, wodurch es leichter wird, Konfigurationswerte bereitzustellen, die für die Provider automatisch validiert werden. Module können sich gegenseitig importieren und Provider exportieren, um eine Hierarchie und eine sauber getrennte Architektur aufzubauen. + +Diese API sollte verwendet werden, wenn die Anwendung komplexer ist und das Deepkit Framework nicht verwendet wird. + +```typescript +import { InjectorModule, InjectorContext } from '@deepkit/injector'; + +const lowLevelModule = new InjectorModule([HttpClient]) + .addExport(HttpClient); + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +const injector = new InjectorContext(rootModule); +``` + +Das Objekt `injector` ist in diesem Fall der Dependency-Injection-Container. Provider können in verschiedene Module aufgeteilt und dann über Modul-Imports an verschiedenen Stellen wieder importiert werden. Dadurch entsteht eine natürliche Hierarchie, die die Hierarchie der Anwendung bzw. Architektur widerspiegelt. +Dem InjectorContext sollte immer das oberste Module in der Hierarchie übergeben werden, auch Root-Module oder App-Module genannt. Der InjectorContext hat dann nur eine vermittelnde Rolle: Aufrufe von `injector.get()` werden schlicht an das Root-Module weitergereicht. Es ist jedoch auch möglich, Provider aus Nicht-Root-Modulen zu erhalten, indem das Module als zweites Argument übergeben wird. + +```typescript +const repository = injector.get(UserRepository); + +const httpClient = injector.get(HttpClient, lowLevelModule); +``` + +Alle Nicht-Root-Module sind standardmäßig gekapselt, sodass alle Provider in diesem Module nur für sich selbst verfügbar sind. Soll ein Provider anderen Modulen zur Verfügung stehen, muss dieser Provider exportiert werden. Durch das Exportieren wandert der Provider in das Eltern-Module der Hierarchie und kann so verwendet werden. + +Um standardmäßig alle Provider an die oberste Ebene, das Root-Module, zu exportieren, kann die Option `forRoot` verwendet werden. Dadurch können alle Provider von allen anderen Modulen genutzt werden. + +```typescript +const lowLevelModule = new InjectorModule([HttpClient]) + .forRoot(); // exportiert alle Provider ins Root-Module +``` + +### App-API + +Sobald das Deepkit Framework verwendet wird, werden Module mit der `@deepkit/app`-API definiert. Diese basiert auf der Module-API, sodass deren Möglichkeiten ebenfalls verfügbar sind. Zusätzlich ist es möglich, mit leistungsfähigen Hooks zu arbeiten und Configuration Loader zu definieren, um noch dynamischere Architekturen abzubilden. + +Das Kapitel [Framework-Module](../app/modules.md) beschreibt dies ausführlicher. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry, HttpBody } from '@deepkit/http'; + +interface User { + username: string; +} + +class Service { + users: User[] = []; +} + +const app = new App({ + providers: [Service], + imports: [new FrameworkModule()], +}); + +const router = app.get(HttpRouterRegistry); + +router.post('/users', (body: HttpBody, service: Service) => { + service.users.push(body); +}); + +router.get('/users', (service: Service): Users => { + return service.users; +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/dependency-injection/injection.md b/website/src/translations/de/documentation/dependency-injection/injection.md new file mode 100644 index 000000000..bddfb9fc8 --- /dev/null +++ b/website/src/translations/de/documentation/dependency-injection/injection.md @@ -0,0 +1,71 @@ +# Injektion + +Es heißt Dependency Injection, da eine Abhängigkeit injiziert wird. Die Injektion erfolgt entweder durch den Benutzer (manuell) oder durch den DI-Container (automatisch). + +## Konstruktorinjektion + +In den meisten Fällen wird die Konstruktorinjektion verwendet. Alle Abhängigkeiten werden als Konstruktor-Argumente angegeben und automatisch vom DI-Container injiziert. + +```typescript +class MyService { + constructor(protected database: Database) { + } +} +``` + +Optionale Abhängigkeiten sollten entsprechend markiert werden, andernfalls könnte ein Error ausgelöst werden, wenn kein Provider gefunden wird. + +```typescript +class MyService { + constructor(protected database?: Database) { + } +} +``` + +## Eigenschaftsinjektion + +Eine Alternative zur Konstruktorinjektion ist die Eigenschaftsinjektion. Diese wird üblicherweise verwendet, wenn die Abhängigkeit optional ist oder der Konstruktor sonst zu voll wäre. Die Properties werden automatisch zugewiesen, sobald die Instanz erstellt wurde (und damit der Konstruktor ausgeführt wurde). + +```typescript +import { Inject } from '@deepkit/core'; + +class MyService { + //erforderlich + protected database!: Inject; + + //oder optional + protected database?: Inject; +} +``` + +## Parameterinjektion + +An verschiedenen Stellen kannst du eine Callback Function definieren, zum Beispiel für HTTP Routes oder CLI Commands. In diesem Fall kannst du Abhängigkeiten als Parameter definieren. +Sie werden automatisch vom DI-Container injiziert. + +```typescript +import { Database } from './db'; + +app.get('/', (database: Database) => { + //... +}); +``` + +## Injector-Kontext + +Falls du Abhängigkeiten dynamisch auflösen möchtest, kannst du `InjectorContext` injizieren und ihn verwenden, um Abhängigkeiten zu beziehen. + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class MyService { + constructor(protected context: InjectorContext) { + } + + getDatabase(): Database { + return this.context.get(Database); + } +} +``` + +Dies ist besonders nützlich bei der Arbeit mit [Dependency Injection Scopes](./scopes.md). \ No newline at end of file diff --git a/website/src/translations/de/documentation/dependency-injection/providers.md b/website/src/translations/de/documentation/dependency-injection/providers.md new file mode 100644 index 000000000..b6a2c9a80 --- /dev/null +++ b/website/src/translations/de/documentation/dependency-injection/providers.md @@ -0,0 +1,311 @@ +# Provider + +Es gibt mehrere Möglichkeiten, Abhängigkeiten im Dependency Injection Container bereitzustellen. Die einfachste Variante ist einfach die Angabe einer Class. Dies ist auch als kurzer ClassProvider bekannt. + +```typescript +new App({ + providers: [UserRepository] +}); +``` + +Dies stellt einen speziellen Provider dar, da nur die Class angegeben wird. Alle anderen Provider müssen als Objektliteral angegeben werden. + +Standardmäßig sind alle Provider als Singletons markiert, sodass zu jedem Zeitpunkt nur eine Instanz existiert. Um bei jeder Auflösung eines Providers eine neue Instanz zu erstellen, kann die Option `transient` verwendet werden. Dadurch werden Classes jedes Mal neu instanziert oder Factories jedes Mal ausgeführt. + +```typescript +new App({ + providers: [{ provide: UserRepository, transient: true }] +}); +``` + +## ClassProvider + +Neben dem kurzen ClassProvider gibt es auch den regulären ClassProvider, der als Objektliteral statt als Class angegeben wird. + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: UserRepository }] +}); +``` + +Dies entspricht diesen beiden: + +```typescript +new App({ + providers: [{ provide: UserRepository }] +}); + +new App({ + providers: [UserRepository] +}); +``` + +Er kann verwendet werden, um einen Provider durch eine andere Class auszutauschen. + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: OtherUserRepository }] +}); +``` + +In diesem Beispiel wird die Class `OtherUserRepository` nun ebenfalls im DI-Container verwaltet und all ihre Abhängigkeiten werden automatisch aufgelöst. + +## ValueProvider + +Statische Werte können mit diesem Provider bereitgestellt werden. + +```typescript +new App({ + providers: [{ provide: OtherUserRepository, useValue: new OtherUserRepository() }] +}); +``` + +Da nicht nur Class-Instanzen als Abhängigkeiten bereitgestellt werden können, kann jeder Wert als `useValue` angegeben werden. Ein symbol oder ein Primitive (string, number, boolean) könnte ebenfalls als Provider-Token verwendet werden. + +```typescript +new App({ + providers: [{ provide: 'domain', useValue: 'localhost' }] +}); +``` + +Primitive Provider-Token müssen mit dem Inject Type als Abhängigkeit deklariert werden. + +```typescript +import { Inject } from '@deepkit/core'; + +class EmailService { + constructor(public domain: Inject) {} +} +``` + +Die Kombination aus einem Inject-Alias und primitiven Provider-Token kann auch verwendet werden, um Abhängigkeiten aus Packages bereitzustellen, die keine Runtime-Type-Informationen enthalten. + +```typescript +import { Inject } from '@deepkit/core'; +import { Stripe } from 'stripe'; + +export type StripeService = Inject; + +new App({ + providers: [{ provide: '_stripe', useValue: new Stripe }] +}); +``` + +Und dann auf der Nutzerseite wie folgt deklariert: + +```typescript +class PaymentService { + constructor(public stripe: StripeService) {} +} +``` + +## ExistingProvider + +Eine Weiterleitung auf einen bereits definierten Provider kann definiert werden. + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useValue: new OtherUserRepository()}, + {provide: UserRepository, useExisting: OtherUserRepository} + ] +}); +``` + +## FactoryProvider + +Eine Function kann verwendet werden, um einen Wert für den Provider bereitzustellen. Diese Function kann auch Parameters enthalten, die wiederum vom DI-Container bereitgestellt werden. So werden andere Abhängigkeiten oder Konfigurationsoptionen zugänglich. + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: () => { + return new OtherUserRepository() + }}, + ] +}); + +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: (domain: RootConfiguration['domain']) => { + return new OtherUserRepository(domain); + }}, + ] +}); + +new App({ + providers: [ + Database, + {provide: OtherUserRepository, useFactory: (database: Database) => { + return new OtherUserRepository(database); + }}, + ] +}); +``` + +## InterfaceProvider + +Neben Classes und Primitives können auch Abstraktionen (Interfaces) bereitgestellt werden. Dies geschieht über die Function `provide` und ist besonders nützlich, wenn der bereitzustellende Wert keine Type-Informationen enthält. + +```typescript +import { provide } from '@deepkit/injector'; + +interface Connection { + write(data: Uint16Array): void; +} + +class Server { + constructor (public connection: Connection) {} +} + +class MyConnection { + write(data: Uint16Array): void {} +} + +new App({ + providers: [ + Server, + provide(MyConnection) + ] +}); +``` + +Wenn mehrere Provider das Connection Interface implementiert haben, wird der letzte Provider verwendet. + +Als Argument für provide() sind alle anderen Provider möglich. + +```typescript +const myConnection = {write: (data: any) => undefined}; + +new App({ + providers: [ + provide({ useValue: myConnection }) + ] +}); + +new App({ + providers: [ + provide({ useFactory: () => myConnection }) + ] +}); +``` + +## Asynchrone Provider + +Das Design von `@deepkit/injector` schließt die Verwendung asynchroner Provider mit einem asynchronen Dependency Injection Container aus. Dies liegt daran, dass auch das Anfordern von Providern asynchron sein müsste, wodurch die gesamte Anwendung auf höchster Ebene asynchron arbeiten müsste. + +Um etwas asynchron zu initialisieren, sollte diese Initialisierung in den Application-Server-Bootstrap verlegt werden, da dort die Events asynchron sein können. Alternativ kann eine Initialisierung manuell ausgelöst werden. + +## Provider konfigurieren + +Konfigurations-Callbacks erlauben es, das Ergebnis eines Providers zu manipulieren. Das ist z. B. nützlich, um eine andere Dependency Injection-Variante zu nutzen, die Method Injection. + +Sie können nur mit der Module API oder der App API verwendet werden und werden oberhalb des Moduls registriert. + +```typescript +class UserRepository { + private db?: Database; + setDatabase(db: Database) { + this.db = db; + } +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider(v => { + v.setDatabase(db); +}); +``` + +`configureProvider` erhält im Callback als ersten Parameter `v` die UserRepository-Instanz, auf der ihre Methods aufgerufen werden können. + +Neben Method-Calls können auch Properties gesetzt werden. + +```typescript +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider(v => { + v.db = new Database(); +}); +``` + +Alle Callbacks werden in eine Queue gestellt und in der Reihenfolge ihrer Definition ausgeführt. + +Die Aufrufe in der Queue werden dann auf das tatsächliche Ergebnis des Providers ausgeführt, sobald der Provider erstellt wird. Das heißt: Bei einem ClassProvider werden sie auf die Class-Instanz angewendet, sobald die Instanz erstellt wurde; bei einem FactoryProvider auf das Ergebnis der Factory; und bei einem ValueProvider auf den Wert. + +Um nicht nur statische Werte, sondern auch andere Provider zu referenzieren, können beliebige Abhängigkeiten in den Callback injiziert werden, indem sie einfach als Arguments definiert werden. Stellen Sie sicher, dass diese Abhängigkeiten im Provider-Scope bekannt sind. + +```typescript +class Database {} + +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository, Database]) +rootModule.configureProvider((v, db: Database) => { + v.db = db; +}); +``` + +## Nominale Typen + +Beachten Sie, dass der an `configureProvider` übergebene Typ, wie im letzten Beispiel `UserRepository`, nicht mittels struktureller Typprüfung, sondern anhand nominaler Typen aufgelöst wird. Das bedeutet zum Beispiel, dass zwei Classes/Interfaces mit gleicher Struktur, aber unterschiedlicher Identität nicht kompatibel sind. Gleiches gilt für `get`-Aufrufe oder wenn eine Abhängigkeit aufgelöst wird. + +Dies unterscheidet sich von der Art, wie die TypeScript-Typprüfung funktioniert, die auf struktureller Typprüfung basiert. Diese Designentscheidung wurde getroffen, um versehentliche Fehlkonfigurationen zu vermeiden (z. B. das Anfordern einer leeren Class, die strukturell mit jeder Class kompatibel ist) und um den Code robuster zu machen. + +Im folgenden Beispiel sind die Classes `User1` und `User2` strukturell kompatibel, aber nicht nominal. Das bedeutet, dass das Anfordern von `User1` nicht `User2` auflöst und umgekehrt. + +```typescript + +class User1 { + name: string = ''; +} + +class User2 { + name: string = ''; +} + +new App({ + providers: [User1, User2] +}); +``` + +Das Erweitern von Classes und das Implementieren von Interfaces stellt eine nominale Beziehung her. + +```typescript +class UserBase { + name: string = ''; +} + +class User extends UserBase { +} + +const app = new App({ + providers: [User2] +}); + +app.get(UserBase); //gibt User zurück +``` + +```typescript +interface UserInterface { + name: string; +} + +class User implements UserInterface { + name: string = ''; +} + +const app = new App({ + providers: [User] +}); + +app.get(); //gibt User zurück +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/dependency-injection/scopes.md b/website/src/translations/de/documentation/dependency-injection/scopes.md new file mode 100644 index 000000000..977d34d2f --- /dev/null +++ b/website/src/translations/de/documentation/dependency-injection/scopes.md @@ -0,0 +1,54 @@ +# Scopes + +Standardmäßig sind alle Provider des DI-Containers Singletons und werden daher nur einmal instanziiert. Das bedeutet, dass es im Beispiel von UserRepository während der gesamten Laufzeit stets nur eine einzige Instanz von UserRepository gibt. Zu keinem Zeitpunkt wird eine zweite Instanz erzeugt, es sei denn, der Benutzer tut dies manuell mit dem "new"-Schlüsselwort. + +Es gibt jedoch verschiedene Anwendungsfälle, in denen ein Provider nur für kurze Zeit oder nur während eines bestimmten Ereignisses instanziiert werden soll. Ein solches Ereignis könnte z. B. eine HTTP-Request oder ein RPC-Call sein. Das würde bedeuten, dass für jedes Ereignis eine neue Instanz erstellt wird und diese, sobald sie nicht mehr verwendet wird, automatisch entfernt wird (durch den Garbage Collector). + +Eine HTTP-Request ist ein klassisches Beispiel für einen Scope. Beispielsweise können Provider wie eine Session, ein User-Objekt oder andere request-bezogene Provider diesem Scope zugeordnet werden. Um einen Scope zu erstellen, wählen Sie einfach einen beliebigen Scope-Namen und geben Sie ihn anschließend bei den Providern an. + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class UserSession {} + +const injector = InjectorContext.forProviders([ + {provide: UserSession, scope: 'http'} +]); +``` + +Sobald ein Scope angegeben ist, kann dieser Provider nicht mehr direkt aus dem DI-Container bezogen werden; der folgende Aufruf schlägt daher fehl: + +```typescript +const session = injector.get(UserSession); //wirft +``` + +Stattdessen muss ein DI-Container für diesen Scope erstellt werden. Das würde jedes Mal passieren, wenn eine HTTP-Request eingeht: + +```typescript +const httpScope = injector.createChildScope('http'); +``` + +Provider, die ebenfalls in diesem Scope registriert sind, können nun über diesen DI-Container mit Scope angefordert werden, ebenso wie alle Provider, die keinen Scope definiert haben. + +```typescript +const session = httpScope.get(UserSession); //funktioniert +``` + +Da alle Provider standardmäßig Singletons sind, liefert jeder Aufruf von `get(UserSession)` innerhalb eines Scoped-Containers stets dieselbe Instanz zurück. Wenn Sie mehrere Scoped-Container erstellen, werden mehrere UserSessions erzeugt. + +Scoped DI-Container haben die Möglichkeit, Werte von außen dynamisch zu setzen. So lassen sich in einem HTTP-Scope beispielsweise einfach die Objekte HttpRequest und HttpResponse setzen. + +```typescript +const injector = InjectorContext.forProviders([ + {provide: HttpResponse, scope: 'http'}, + {provide: HttpRequest, scope: 'http'}, +]); + +httpServer.on('request', (req, res) => { + const httpScope = injector.createChildScope('http'); + httpScope.set(HttpRequest, req); + httpScope.set(HttpResponse, res); +}); +``` + +Anwendungen, die das Deepkit-Framework verwenden, haben standardmäßig einen `http`-, einen `rpc`- und einen `cli`-Scope. Siehe dazu die Kapitel [CLI](../cli.md), [HTTP](../http.md) und [RPC](../rpc.md). \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem.md b/website/src/translations/de/documentation/filesystem.md new file mode 100644 index 000000000..b97691b3b --- /dev/null +++ b/website/src/translations/de/documentation/filesystem.md @@ -0,0 +1,273 @@ +# Deepkit Filesystem + +Deepkit Filesystem ist eine Filesystem-Abstraktion für lokale und entfernte Filesystems. Sie ermöglicht es, mit Dateien und Verzeichnissen auf einheitliche Weise zu arbeiten, unabhängig davon, ob die Dateien lokal oder auf einem entfernten Server gespeichert sind. + +Unterstützte Filesystems standardmäßig: + +- Lokales Filesystem (im Paket `@deepkit/filesystem`) +- Memory (im Paket `@deepkit/filesystem`) +- FTP (im Paket `@deepkit/filesystem-ftp`) +- SFTP (im Paket `@deepkit/filesystem-sftp`) +- AWS S3 (im Paket `@deepkit/filesystem-aws-s3`) +- Google Cloud Filesystem (im Paket `@deepkit/filesystem-google`) + +## Installation + +```bash +npm install @deepkit/filesystem +``` + +Hinweis: Wenn du NPM nicht verwendest, stelle sicher, dass Peer-Abhängigkeiten korrekt installiert sind. + +## Verwendung + +```typescript +import { Filesystem, FilesystemLocalAdapter } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter('/path/to/my/files'); +const filesystem = new Filesystem(adapter); + +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} + +await filesystem.write('myFile.txt', 'Hello World'); +const file = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); + +const content = await filesystem.read('myFile.txt'); +``` + + +## Dateien auflisten + +Zum Auflisten von Dateien verwende die Methode `files()`. Sie gibt ein Array von `File`-Objekten zurück. + +```typescript +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} +``` + +Wenn das Array leer ist, existiert der Ordner nicht oder es befinden sich keine Dateien darin. + +Um alle Dateien rekursiv aufzulisten, verwende die Methode `allFiles()`. + +```typescript +const files = await filesystem.allFiles(); +for (const file of files) { + console.log(file.path); +} +``` + +## Datei lesen + +Um eine Datei zu lesen, verwende die Methode `read()`. Sie gibt den Dateiinhalt als `Uint8Array` zurück. + +```typescript +const content = await filesystem.read('myFile.txt'); +``` + +Um sie als Text zu lesen, verwende die Methode `readAsText()`. Sie gibt einen String zurück. + +```typescript +const content = await filesystem.readAsText('myFile.txt'); +``` + +## Datei schreiben + +Um eine Datei zu schreiben, verwende die Methode `write()`. Sie akzeptiert einen `Uint8Array` oder einen String. + +```typescript +await filesystem.write('myFile.txt', 'Hello World'); +``` + +Um aus einer lokalen Datei zu schreiben, verwende die Methode `writeFile()`. + +Beachte, dass das erste Argument das Verzeichnis ist, nicht der Dateiname. Der Dateiname ist der Basename der bereitgestellten Datei. +Du kannst den Dateinamen ändern, indem du ein {name: 'myFile.txt'}-Objekt als zweites Argument übergibst. Wenn keine Dateierweiterung angegeben ist, +wird die Erweiterung automatisch erkannt. + +```typescript +const path = await filesystem.writeFile('/', { path: '/path/to/local/file.txt' }); + +// funktioniert mit UploadedFile +router.post('/upload', async (body: HttpBody<{ file: UploadedFile }>, filesystem: Filesystem, session: Session) => { + const user = session.getUser(); + const path = await filesystem.writeFile('/user-images', body.file, { name: `user-${user.id}` }); + //path = /user-images/user-123.jpg +}); +``` + + +## Datei löschen + +Um eine Datei zu löschen, verwende die Methode `delete()`. + +```typescript +await filesystem.delete('myFile.txt'); +``` + +Dies löscht die Datei `myFile.txt` im Root-Verzeichnis. Wenn der Pfad ein Verzeichnis ist, wird ein Error geworfen. + +## Verzeichnis löschen + +Um ein Verzeichnis zu löschen, verwende die Methode `deleteDirectory()`. Dies löscht das Verzeichnis sowie alle Dateien und Verzeichnisse darin. + +```typescript +await filesystem.deleteDirectory('myFolder'); +``` + +## Verzeichnis erstellen + +Um ein Verzeichnis zu erstellen, verwende die Methode `createDirectory()`. + +```typescript +await filesystem.makeDirectory('myFolder'); +``` + +## Datei verschieben + +Um eine Datei oder ein Verzeichnis zu verschieben, verwende die Methode `move()`. Sie akzeptiert ein `File`-Objekt oder einen Pfad. + +```typescript +await filesystem.move('myFile.txt', 'myFolder/myFile.txt'); +``` + +Das Verzeichnis `myFolder` wird erstellt, falls es nicht existiert. + +## Datei kopieren + +Um eine Datei oder ein Verzeichnis zu kopieren, verwende die Methode `copy()`. Sie akzeptiert ein `File`-Objekt oder einen Pfad. + +```typescript +await filesystem.copy('myFile.txt', 'myFolder/myFile.txt'); +``` + +Das Verzeichnis `myFolder` wird erstellt, falls es nicht existiert. + +## Dateiinformationen + +Um Informationen über eine Datei zu erhalten, verwende die Methode `get()`. Sie gibt ein `File`-Objekt zurück. + +```typescript +const file: FilesystemFile = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); +``` + +Es werden der Pfad, die Dateigröße in Bytes und das letzte Änderungsdatum zurückgegeben. Das Änderungsdatum kann undefined sein, wenn der Adapter dies nicht unterstützt. + +Das File-Objekt bietet außerdem einige praktische Methoden: + +```typescript +interface FilesystemFile { + path: string; + type: FileType; //Datei oder Verzeichnis + size: number; + lastModified?: Date; + /** + * Sichtbarkeit der Datei. + * + * Beachte, dass einige Adapter das Lesen der Sichtbarkeit einer Datei ggf. nicht unterstützen. + * In diesem Fall ist die Sichtbarkeit immer 'private'. + * + * Einige Adapter unterstützen das Lesen der Sichtbarkeit pro Datei, jedoch nicht beim Auflisten von Dateien. + * In diesem Fall musst du zusätzlich `filesystem.get(file)` aufrufen, um die Sichtbarkeit zu laden. + */ + visibility: FileVisibility; //public oder private + constructor(path: string, type?: FileType); + /** + * Gibt true zurück, wenn diese Datei ein symbolischer Link ist. + */ + isFile(): boolean; + /** + * Gibt true zurück, wenn diese Datei ein Verzeichnis ist. + */ + isDirectory(): boolean; + /** + * Gibt den Namen (Basename) der Datei zurück. + */ + get name(): string; + /** + * Gibt true zurück, wenn sich diese Datei im angegebenen Verzeichnis befindet. + */ + inDirectory(directory: string): boolean; + /** + * Gibt das Verzeichnis (dirname) der Datei zurück. + */ + get directory(): string; + /** + * Gibt die Dateierweiterung zurück, oder einen leeren String, wenn nicht vorhanden oder ein Verzeichnis. + */ + get extension(): string; +} +``` + +## Datei existiert + +Um zu prüfen, ob eine Datei existiert, verwende die Methode `exists()`. + +```typescript + +if (await filesystem.exists('myFile.txt')) { + console.log('File exists'); +} +``` + +## Dateisichtbarkeit + +Deepkit Filesystems unterstützt eine einfache Abstraktion für Dateisichtbarkeit, mit der Dateien public oder private gemacht werden können. + +Dies ist z. B. für S3 oder Google Cloud Filesystem nützlich. Beim lokalen Filesystem werden die Dateiberechtigungen abhängig von der Sichtbarkeit gesetzt. + +Um die Sichtbarkeit einer Datei festzulegen, verwende entweder die Methode `setVisibility()` oder übergib die Sichtbarkeit als drittes Argument an `write()`. + +```typescript +await filesystem.setVisibility('myFile.txt', 'public'); +await filesystem.write('myFile.txt', 'Hello World', 'public'); +``` + +Um die Sichtbarkeit einer Datei abzurufen, verwende die Methode `getVisibility()` oder prüfe `FilesystemFile.visibility`. + +```typescript +const visibility = await filesystem.getVisibility('myFile.txt'); +const file = await filesystem.get('myFile.txt'); +console.log(file.visibility); +``` + +Beachte, dass einige Adapter das Abrufen der Sichtbarkeit über `sotrage.files()` ggf. nicht unterstützen. In diesem Fall ist die Sichtbarkeit immer `unknown`. + +## Öffentliche URL der Datei + +Um die öffentliche URL einer Datei zu erhalten, verwende die Methode `publicUrl()`. Dies funktioniert nur, wenn die Datei public ist und der Adapter dies unterstützt. + +```typescript +const url = filesystem.publicUrl('myFile.txt'); +``` + +Wenn der Adapter keine öffentlichen URLs unterstützt, übernimmt die Filesystem-Abstraktion dies, indem sie eine URL über `option.baseUrl` generiert. + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + baseUrl: 'https://my-domain.com/assets/' +}); + +const url = await filesystem.publicUrl('myFile.txt'); +console.log(url); //https://my-domain.com/assets/myFile.txt +``` + +## Filesystem-Optionen + +Der `Filesystem`-Konstruktor akzeptiert als zweites Argument ein Options-Objekt. + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + visibility: 'private', //Standard-Sichtbarkeit für Dateien + directoryVisibility: 'private', //Standard-Sichtbarkeit für Verzeichnisse + pathNormalizer: (path: string) => path, //normalisiert den Pfad. Standardmäßig wird `[^a-zA-Z0-9\.\-\_]` durch `-` ersetzt. + urlBuilder: (path: string) => path, //erstellt die öffentliche URL für eine Datei. Standardmäßig: baseUrl + path + baseUrl: '', //Basis-URL für öffentliche URLs +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/app.md b/website/src/translations/de/documentation/filesystem/app.md new file mode 100644 index 000000000..5e92fc8f8 --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/app.md @@ -0,0 +1,85 @@ +# App + +Obwohl Deepkit Storage eigenständig funktioniert, möchtest du es wahrscheinlich in deiner Deepkit App mit Dependency Injection verwenden. + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +const app = new App({ + providers: [ + provideFilesystem(new FilesystemLocalAdapter({root: __dirname + '/public'})), + ] +}); + +app.command('write', async (content: string, fs: Filesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: Filesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + + +## Multiple Filesystems + +Du kannst mehrere Filesystems gleichzeitig verwenden. Dazu registrierst du sie mit `provideNamedFilesystem('name', ...)` und erhältst die Filesystem-Instanz mit +`NamedFilesystem<'name'>`. + +```typescript +import { App } from '@deepkit/app'; +import { NamedFilesystem, FilesystemLocalAdapter, provideNamedFilesystem } from '@deepkit/filesystem'; + +type PrivateFilesystem = NamedFilesystem<'private'>; +type PublicFilesystem = NamedFilesystem<'public'>; + +const app = new App({ + providers: [ + provideNamedFilesystem('private', new FilesystemLocalAdapter({root: '/tmp/dir1'})), + provideNamedFilesystem('public', new FilesystemLocalAdapter({root: '/tmp/dir2'})), + ] +}); + +app.command('write', async (content: string, fs: PublicFilesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: PublicFilesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + +## Konfiguration + +Es ist oft sinnvoll, einen Adapter über Konfigurationsoptionen zu konfigurieren. Zum Beispiel möchtest du vielleicht das Root-Verzeichnis des lokalen Adapters konfigurieren +oder Zugangsdaten für den AWS S3- oder Google-Storage-Adapter. + +Dazu kannst du [Configuration Injection](../app/configuration.md) verwenden. + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +class MyConfig { + fsRoot: string = '/tmp'; +} + +const app = new App({ + config: MyConfig, + providers: [ + provideFilesystem( + (config: MyConfig) => new FilesystemLocalAdapter({root: config.fsRoot}) + ), + ] +}); + +app.loadConfigFromEnv({prefix: 'APP_'}) +app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/aws-s3.md b/website/src/translations/de/documentation/filesystem/aws-s3.md new file mode 100644 index 000000000..822ff7a3f --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/aws-s3.md @@ -0,0 +1,50 @@ +# AWS S3-Dateisystem + +Der AWS S3 Filesystem-Adapter ermöglicht es Ihnen, den AWS S3 Service als Deepkit Filesystem zu verwenden. + +Es ist Teil von `@deepkit/filesystem-aws-s3`, das separat installiert werden muss. + +```sh +npm install @deepkit/filesystem-aws-s3 +``` + +## Verwendung + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemAwsS3Adapter } from '@deepkit/filesystem-aws-s3'; + +const adapter = new FilesystemAwsS3Adapter({ + bucket: 'my-bucket', + path: 'starting-path/', // optional + region: 'eu-central-1', + acccessKeyId: '...', + secretAccessKey: '...' +}); +const filesystem = new Filesystem(adapter); +``` + +Hinweis: Sie sollten Ihre Anmeldeinformationen nicht direkt im Code speichern. Verwenden Sie stattdessen Umgebungsvariablen oder [App-Konfiguration](./app.md#configuration). + +Dieser Adapter verwendet den S3-Client von [@aws-sdk/client-s3](https://npmjs.com/package/@aws-sdk/client-s3). +Alle Konfigurationsoptionen können an den Adapter-Konstruktor übergeben werden. + +## Berechtigungen + +Sie können konfigurieren, welche Sichtbarkeit eine Datei bei der Erstellung hat. + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +Die Datei `/hello-public.txt` wird mit der ACL `public-read` erstellt und kann von jedem über ihre URL gelesen werden. Die URL kann über `filesystem.publicUrl` abgerufen werden. + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://my-bucket.s3.eu-central-1.amazonaws.com/starting-path/hello-public.txt +``` + +Um die Sichtbarkeit zu verwenden, müssen Sie objektbasierte ACLs in Ihrem S3-Bucket aktivieren. \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/database.md b/website/src/translations/de/documentation/filesystem/database.md new file mode 100644 index 000000000..af0e228c3 --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/database.md @@ -0,0 +1,23 @@ +# Datenbank-Dateisystem + +Dieser Adapter ermöglicht es, eine Datenbank-ORM als Dateisystem-Backend zu verwenden. Das bedeutet, dass alle Dateien und Ordner in der Datenbank gespeichert werden. + +```sh +npm install @deepkit/filesystem-database @deepkit/orm +``` + +## Verwendung + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemDatabaseAdapter } from '@deepkit/filesystem-database'; + +const database = new Database(new MemoryDatabaseAdapter()); +// const database = new Database(new PostgresDatabaseAdapter()); +// const database = new Database(new MongoDatabaseAdapter()); +// const database = new Database(new MysqlDatabaseAdapter()); +// const database = new Database(new SQLiteDatabaseAdapter()); + +const adapter = new FilesystemDatabaseAdapter({ database }); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/ftp.md b/website/src/translations/de/documentation/filesystem/ftp.md new file mode 100644 index 000000000..38c9994fe --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/ftp.md @@ -0,0 +1,55 @@ +# FTP-Dateisystem + +Dieser Adapter ermöglicht es, einen FTP-Server als Dateisystem zu verwenden. + +Er ist Teil von `@deepkit/filesystem-ftp`, das separat installiert werden muss. + +```sh +npm install @deepkit/filesystem-ftp +``` + +## Verwendung + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemFtpAdapter } from '@deepkit/filesystem-ftp'; + +const adapter = new FilesystemFtpAdapter({ + root: 'folder', + host: 'localhost', + port: 21, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +Hinweis: Sie sollten Ihre Zugangsdaten nicht direkt im Code speichern. Verwenden Sie stattdessen Umgebungsvariablen oder [App-Konfiguration](./app.md#configuration). + +## Berechtigungen + +Wenn der FTP-Server in einer Unix-Umgebung läuft, können Sie die Berechtigungen der Dateien und Ordner über die Option `permissions` festlegen, genauso wie beim [lokalen Dateisystem-Adapter](./local.md). + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +Hier wird die Datei `/hello-public.txt` mit den Berechtigungen `0o644` und `/hello-private.txt` mit `0o600` erstellt. \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/google-storage.md b/website/src/translations/de/documentation/filesystem/google-storage.md new file mode 100644 index 000000000..efdfaedf1 --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/google-storage.md @@ -0,0 +1,49 @@ +# Google Storage-Dateisystem + +Dieser Adapter ermöglicht die Verwendung von Google Storage als Dateisystem. + +Er ist Teil von `@deepkit/filesystem-google`, das separat installiert werden muss. + +```sh +npm install @deepkit/filesystem-google +``` + +## Verwendung + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemGoogleAdapter } from '@deepkit/filesystem-google'; + +const adapter = new FilesystemGoogleAdapter({ + bucket: 'my-bucket', + path: 'starting-path/', //optional + projectId: '...', + keyFilename: 'path/to/service-account-key.json' +}); +const filesystem = new Filesystem(adapter); +``` + +Hinweis: Sie sollten Ihre Zugangsdaten nicht direkt im Code speichern. Verwenden Sie stattdessen Umgebungsvariablen oder [App‑Konfiguration](./app.md#configuration). + +Dieser Adapter verwendet den Google‑Storage‑Client von [@google-cloud/storage](https://npmjs.com/package/@google-cloud/storage). +Alle dessen Konfigurationsoptionen können an den Adapter‑Konstruktor übergeben werden. + +## Berechtigungen + +Sie können konfigurieren, welche Sichtbarkeit eine Datei bei der Erstellung hat. + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +Die Datei `/hello-public.txt` wird mit der ACL `public: true` erstellt und kann von jedem über ihre URL gelesen werden. Die URL kann mittels `filesystem.publicUrl` abgerufen werden. + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://storage.googleapis.com/my-bucket/starting-path/hello-public.txt +``` + +Um die Sichtbarkeit verwenden zu können, müssen Sie objektbasierte ACLs in Ihrem Google‑Storage‑Bucket aktivieren. \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/local.md b/website/src/translations/de/documentation/filesystem/local.md new file mode 100644 index 000000000..36266bc47 --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/local.md @@ -0,0 +1,49 @@ +# Lokales Dateisystem + +Der lokale Dateisystem-Adapter ist eine der am häufigsten verwendeten Optionen und bietet Zugriff auf das Dateisystem, auf dem die Anwendung läuft. + +Er ist Teil von `@deepkit/filesystem` und verwendet die `fs/promises`-API von Node unter der Haube, sodass keine zusätzliche Installation erforderlich ist. + +## Verwendung + +```typescript +import { FilesystemLocalAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter({ root: '/path/to/files' }); +const filesystem = new Filesystem(adapter); +``` + +Hier gibt die Option `root` das Wurzelverzeichnis des Dateisystems an. Alle Pfade sind relativ zu diesem Wurzelverzeichnis. + +```typescript +// liest die Datei /path/to/files/hello.txt +const content: string = await filesystem.readAsText('/hello.txt'); +``` + +## Berechtigungen + +Sie können konfigurieren, welche Berechtigungen das Dateisystem beim Erstellen von Dateien und Verzeichnissen verwenden soll. Jede Kategorie (file, directory) kann separat in zwei Sichtbarkeiten konfiguriert werden: `public` und `private`. + +```typescript +const adapter = new FilesystemLocalAdapter({ + root: '/path/to/files', + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +Hier wird die Datei `/hello-public.txt` mit den Berechtigungen `0o644` und `/hello-private.txt` mit `0o600` erstellt. \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/memory.md b/website/src/translations/de/documentation/filesystem/memory.md new file mode 100644 index 000000000..a4720ef25 --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/memory.md @@ -0,0 +1,15 @@ +# In-Memory-Dateisystem + +Beim In-Memory-Dateisystem wird das Dateisystem im Arbeitsspeicher gehalten. Das bedeutet, dass das Dateisystem nicht persistent ist und beim Beenden der Anwendung verloren geht. +Dies ist insbesondere für Testzwecke nützlich. + +Es ist Teil von `@deepkit/filesystem`, daher ist keine zusätzliche Installation erforderlich. + +## Verwendung + +```typescript +import { FilesystemMemoryAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemMemoryAdapter(); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/filesystem/sftp.md b/website/src/translations/de/documentation/filesystem/sftp.md new file mode 100644 index 000000000..1322e0c93 --- /dev/null +++ b/website/src/translations/de/documentation/filesystem/sftp.md @@ -0,0 +1,57 @@ +# sFTP (SSH) Dateisystem + +Dieser Adapter ermöglicht es, einen sFTP-(SSH)-Server als Dateisystem zu verwenden. + +Es ist Teil von `@deepkit/filesystem-sftp`, das separat installiert werden muss. + +```sh +npm install @deepkit/filesystem-sftp +``` + +## Verwendung + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemSftpAdapter } from '@deepkit/filesystem-sftp'; + +const adapter = new FilesystemSftpAdapter({ + root: 'folder', + host: 'localhost', + port: 22, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +Hinweis: Sie sollten Ihre Zugangsdaten nicht direkt im Code speichern. Verwenden Sie stattdessen Umgebungsvariablen oder [App-Konfiguration](./app.md#configuration). + +Dieser Adapter verwendet den sFTP-Client von [ssh2-sftp-client](https://npmjs.com/package/ssh2-sftp-client). Alle seine Konfigurationsoptionen können an den Adapter-Konstruktor übergeben werden. + +## Berechtigungen + +Wenn der FTP-Server in einer Unix-Umgebung läuft, können Sie die Berechtigungen der Dateien und Ordner über die Option `permissions` festlegen, genau wie beim [lokalen Dateisystem-Adapter](./local.md). + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +Hier wird die Datei `/hello-public.txt` mit den Berechtigungen `0o644` und `/hello-private.txt` mit `0o600` erstellt. \ No newline at end of file diff --git a/website/src/translations/de/documentation/framework.md b/website/src/translations/de/documentation/framework.md new file mode 100644 index 000000000..f3d609aa2 --- /dev/null +++ b/website/src/translations/de/documentation/framework.md @@ -0,0 +1,215 @@ +# Deepkit Framework + +Das Deepkit Framework basiert auf [Deepkit App](./app.md) in `@deepkit/app` und stellt das Modul `FrameworkModule` in `@deepkit/framework` bereit, das in Ihrer Anwendung importiert werden kann. + +Die `App`-Abstraktion bringt: + +- CLI-Befehle +- Konfigurationsladen (Environment, Dotfiles, benutzerdefiniert) +- Modul-System +- Leistungsfähiger Service-Container +- Registry und Hooks für Controller, Provider, Listener und mehr + +Das Modul `FrameworkModule` bringt zusätzliche Funktionen: + +- Anwendungsserver + - HTTP-Server + - RPC-Server + - Multi-Prozess-Lastverteilung + - SSL +- Debugging-CLI-Befehle +- Konfiguration/Befehle für Database Migration +- Debugging/Profiler-GUI über die Option `{debug: true}` +- Interaktive API-Dokumentation (ähnlich Swagger) +- Provider für DatabaseRegistry, ProcessLocking, Broker, Sessions +- Integration-Test-APIs + +Sie können Anwendungen mit oder ohne das `FrameworkModule` schreiben. + +## Installation + +Das Deepkit Framework basiert auf [Deepkit App](./app.md). Stellen Sie sicher, dass Sie dessen Installationsanweisungen befolgt haben. +Falls ja, können Sie das Deepkit Framework installieren und das `FrameworkModule` in Ihre `App` importieren. + +```sh +npm install @deepkit/framework +``` + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +Da die App jetzt das `FrameworkModule` importiert, sehen wir, dass mehr Befehle verfügbar sind, gruppiert nach Themen. + +Einer davon ist `server:start`, der den HTTP-Server startet. Um ihn zu verwenden, müssen wir mindestens eine HTTP-Route registrieren. + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return 'Hello World'; +}) + +app.run(); +``` + +Wenn Sie den Befehl `server:start` erneut ausführen, sehen Sie, dass der HTTP-Server nun gestartet ist und die Route `/` verfügbar ist. + +```sh +$ ./node_modules/.bin/ts-node ./app.ts server:start +``` + +```sh +$ curl http://localhost:8080/ +Hello World +``` + +Um Anfragen zu bedienen, lesen Sie bitte das Kapitel [HTTP](http.md) oder [RPC](rpc.md). Im Kapitel [App](app.md) erfahren Sie mehr über CLI-Befehle. + +## App + +Die `App`-Klasse ist der Haupteinstiegspunkt für Ihre Anwendung. Sie ist verantwortlich für das Laden aller Module, der Konfiguration und das Starten der Anwendung. +Sie ist außerdem dafür verantwortlich, alle CLI-Befehle zu laden und auszuführen. Module wie FrameworkModule stellen zusätzliche Befehle bereit, registrieren Event-Listener, +stellen Controller für HTTP/RPC, Service-Provider und so weiter bereit. + +Dieses `app`-Objekt kann auch verwendet werden, um auf den Dependency-Injection-Container zuzugreifen, ohne einen CLI-Controller auszuführen. + +```typescript +const app = new App({ + imports: [new FrameworkModule] +}); + +// Zugriff auf alle registrierten Services +const eventDispatcher = app.get(EventDispatcher); +``` + +Sie können den `EventDispatcher` abrufen, weil das `FrameworkModule` ihn als Service-Provider registriert, wie viele andere (Logger, ApplicationServer und [vieles mehr](https://github.com/deepkit/deepkit-framework/blob/master/packages/framework/src/module.ts)). + +Sie können auch Ihren eigenen Service registrieren. + +```typescript + +class MyService { + constructor(private logger: Logger) { + } + + helloWorld() { + this.logger.log('Hello World'); + } +} + +const app = new App({ + providers: [MyService], + imports: [new FrameworkModule] +}); + +const service = app.get(MyService); + +service.helloWorld(); +``` + +### Debugger + +Die Konfigurationswerte Ihrer Anwendung und aller Module können im Debugger angezeigt werden. Aktivieren Sie die Option `debug` im `FrameworkModule` und öffnen Sie `http://localhost:8080/_debug/configuration`. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + debug: true, + }) + ] +}).run(); +``` + +![Debugger-Konfiguration](/assets/documentation/framework/debugger-configuration.png) + +Sie können auch `ts-node app.ts app:config` verwenden, um alle verfügbaren Konfigurationsoptionen, den aktiven Wert, ihren Standardwert, Beschreibung und Datentyp anzuzeigen. + +```sh +$ ts-node app.ts app:config +Application config +┌─────────┬───────────────┬────────────────────────┬────────────────────────┬─────────────┬───────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼───────────────┼────────────────────────┼────────────────────────┼─────────────┼───────────┤ +│ 0 │ 'pageTitle' │ 'Other title' │ 'Cool site' │ '' │ 'string' │ +│ 1 │ 'domain' │ 'example.com' │ 'example.com' │ '' │ 'string' │ +│ 2 │ 'port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 3 │ 'databaseUrl' │ 'mongodb://localhost/' │ 'mongodb://localhost/' │ '' │ 'string' │ +│ 4 │ 'email' │ false │ false │ '' │ 'boolean' │ +│ 5 │ 'emailSender' │ undefined │ undefined │ '' │ 'string?' │ +└─────────┴───────────────┴────────────────────────┴────────────────────────┴─────────────┴───────────┘ +Modules config +┌─────────┬──────────────────────────────┬─────────────────┬─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼──────────────────────────────┼─────────────────┼─────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────┤ +│ 0 │ 'framework.host' │ 'localhost' │ 'localhost' │ '' │ 'string' │ +│ 1 │ 'framework.port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 2 │ 'framework.httpsPort' │ undefined │ undefined │ 'If httpsPort and ssl is defined, then the https server is started additional to the http-server.' │ 'number?' │ +│ 3 │ 'framework.selfSigned' │ undefined │ undefined │ 'If for ssl: true the certificate and key should be automatically generated.' │ 'boolean?' │ +│ 4 │ 'framework.keepAliveTimeout' │ undefined │ undefined │ '' │ 'number?' │ +│ 5 │ 'framework.path' │ '/' │ '/' │ '' │ 'string' │ +│ 6 │ 'framework.workers' │ 1 │ 1 │ '' │ 'number' │ +│ 7 │ 'framework.ssl' │ false │ false │ 'Enables HTTPS server' │ 'boolean' │ +│ 8 │ 'framework.sslOptions' │ undefined │ undefined │ 'Same interface as tls.SecureContextOptions & tls.TlsOptions.' │ 'any' │ +... +``` + +## Anwendungsserver + +## Dateistruktur + +## Auto-CRUD + +## Events + +Das Deepkit-Framework wird mit verschiedenen Event-Tokens geliefert, auf die Event-Listener registriert werden können. + +Siehe das Kapitel [Events](./app/events.md), um mehr darüber zu erfahren, wie Events funktionieren. + +### Events senden + +Events werden über die `EventDispatcher`-Klasse gesendet. In einer Deepkit-App kann dieser über Dependency Injection bereitgestellt werden. + +```typescript +import { cli, Command } from '@deepkit/app'; +import { EventDispatcher } from '@deepkit/event'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected eventDispatcher: EventDispatcher) { + } + + async execute() { + this.eventDispatcher.dispatch(UserAdded, new UserEvent({ username: 'Peter' })); + } +} +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/framework/database.md b/website/src/translations/de/documentation/framework/database.md new file mode 100644 index 000000000..d7f123a08 --- /dev/null +++ b/website/src/translations/de/documentation/framework/database.md @@ -0,0 +1,226 @@ +# Datenbank + +Deepkit verfügt über eine eigene, leistungsfähige Datenbank-Abstraktionsbibliothek namens Deepkit ORM. Sie ist eine Object-Relational Mapping (ORM) Bibliothek, die die Arbeit mit SQL-Datenbanken und MongoDB erleichtert. + +Obwohl Sie jede beliebige Datenbankbibliothek verwenden können, empfehlen wir Deepkit ORM, da es die schnellste TypeScript-Datenbank-Abstraktionsbibliothek ist, die perfekt in das Deepkit Framework integriert ist und viele Features bietet, die Ihren Workflow und Ihre Effizienz verbessern. + +Dieses Kapitel erklärt, wie Sie Deepkit ORM mit Ihrer Deepkit App verwenden. Alle Informationen zu Deepkit ORM finden Sie im Kapitel [ORM](../orm.md). + +## Datenbank-Klassen + +Die einfachste Möglichkeit, das Objekt `Database` von Deepkit ORM innerhalb der Anwendung zu verwenden, besteht darin, eine davon abgeleitete Klasse zu registrieren. + +```typescript +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + constructor() { + super( + new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), + [User] + ); + } +} +``` + +Erstellen Sie eine neue Klasse und geben Sie in deren Konstruktor den Adapter mit seinen Parametern an und fügen Sie als zweiten Parameter alle Entity-Modelle hinzu, die mit dieser Datenbank verbunden werden sollen. + +Sie können diese Datenbankklasse nun als Provider registrieren. Außerdem aktivieren wir `migrateOnStartup`, wodurch beim Bootstrap automatisch alle Tabellen in Ihrer Datenbank erstellt werden. Das ist ideal für schnelles Prototyping, wird jedoch für ein ernsthaftes Projekt oder den Produktionseinsatz nicht empfohlen. Verwenden Sie dort reguläre Datenbank-Migrationen. + +Wir aktivieren außerdem `debug`, womit Sie beim Start des Servers der Anwendung den Debugger öffnen und Ihre Datenbank-Modelle direkt im integrierten ORM Browser verwalten können. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { SQLiteDatabase } from './database.ts'; + +new App({ + providers: [SQLiteDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}).run(); +``` + +Sie können nun überall mittels Dependency Injection auf `SQLiteDatabase` zugreifen: + +```typescript +import { SQLiteDatabase } from './database.ts'; + +export class Controller { + constructor(protected database: SQLiteDatabase) {} + + @http.GET() + async startPage(): Promise { + // alle Benutzer zurückgeben + return await this.database.query(User).find(); + } +} +``` + +## Konfiguration + +In vielen Fällen sollen Ihre Verbindungs-Credentials konfigurierbar sein. Beispielsweise möchten Sie für Tests eine andere Datenbank verwenden als für die Produktion. Das können Sie mithilfe der Option `config` der Klasse `Database` tun. + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { PostgresDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +type DbConfig = Pick; + +export class MainDatabase extends Database { + constructor(config: DbConfig) { + super(new PostgresDatabaseAdapter({ + host: config.databaseHost, + user: config.databaseUser, + password: config.databasePassword, + }), [User]); + } +} +``` + +```typescript +//config.ts +export class AppConfig { + databaseHost: string = 'localhost'; + databaseUser: string = 'postgres'; + databasePassword: string = ''; +} +``` + +```typescript +//app.ts +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { MainDatabase } from './database.ts'; +import { AppConfig } from './config.ts'; + +const app = new App({ + config: AppConfig, + providers: [MainDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}); +app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper', envFilePath: ['local.env', 'prod.env']}); +app.run(); +``` + +Da wir nun `loadConfigFromEnv` verwenden, können wir die Datenbank-Zugangsdaten über Umgebungsvariablen setzen. + +```sh +APP_DATABASE_HOST=localhost APP_DATABASE_USER=postgres ts-node app.ts server:start +``` + +oder in der Datei `local.env` und `ts-node app.ts server:start` ohne zuvor gesetzte Umgebungsvariablen starten. + +```sh +APP_DATABASE_HOST=localhost +APP_DATABASE_USER=postgres +``` + +## Mehrere Datenbanken + +Sie können beliebig viele Datenbank-Klassen hinzufügen und frei benennen. Achten Sie darauf, den Namen jeder Datenbank zu ändern, damit es bei der Verwendung des Deepkit ORM Browser nicht zu Konflikten kommt. + +## Daten verwalten + +Sie haben nun alles eingerichtet, um Ihre Datenbankdaten mit dem Deepkit ORM Browser zu verwalten. Um den Deepkit ORM Browser zu öffnen und den Inhalt zu managen, schreiben Sie alle Schritte von oben in die Datei `app.ts` und starten Sie den Server. + +```sh +$ ts-node app.ts server:start +2021-06-11T15:08:54.330Z [LOG] Start HTTP server, using 1 workers. +2021-06-11T15:08:54.333Z [LOG] Migrate database default +2021-06-11T15:08:54.336Z [LOG] RPC DebugController deepkit/debug/controller +2021-06-11T15:08:54.337Z [LOG] RPC OrmBrowserController orm-browser/controller +2021-06-11T15:08:54.337Z [LOG] HTTP OrmBrowserController +2021-06-11T15:08:54.337Z [LOG] GET /_orm-browser/query httpQuery +2021-06-11T15:08:54.337Z [LOG] HTTP StaticController +2021-06-11T15:08:54.337Z [LOG] GET /_debug/:any serviceApp +2021-06-11T15:08:54.337Z [LOG] HTTP listening at http://localhost:8080/ +``` + +Sie können nun http://localhost:8080/_debug/database/default öffnen. + +![Debugger Datenbank](/assets/documentation/framework/debugger-database.png) + +Sie sehen das ER-Diagramm (Entity-Relationship). Im Moment ist nur eine Entity verfügbar. Wenn Sie weitere mit Relationen hinzufügen, sehen Sie alle Informationen auf einen Blick. + +Wenn Sie in der linken Seitenleiste auf `User` klicken, können Sie dessen Inhalte verwalten. Klicken Sie auf das `+`-Symbol und ändern Sie den Titel des neuen Datensatzes. Nachdem Sie die erforderlichen Werte (wie den Benutzernamen) geändert haben, klicken Sie auf `Confirm`. Dadurch werden alle Änderungen in die Datenbank übernommen und dauerhaft gemacht. Die Auto-Increment-ID wird automatisch vergeben. + +![Debugger Datenbank User](/assets/documentation/framework/debugger-database-user.png) + +## Mehr erfahren + +Um mehr darüber zu erfahren, wie `SQLiteDatabase` funktioniert, lesen Sie bitte das Kapitel [Datenbank](../orm.md) und dessen Unterkapitel, wie das Abfragen von Daten, das Manipulieren von Daten über Sessions, das Definieren von Relationen und vieles mehr. +Bitte beachten Sie, dass sich die Kapitel dort auf die eigenständige Bibliothek `@deepkit/orm` beziehen und keine Dokumentation über den Teil des Deepkit Frameworks enthalten, den Sie oben in diesem Kapitel gelesen haben. In der Standalone-Bibliothek instanziieren Sie Ihre Datenbankklasse manuell, zum Beispiel über `new SQLiteDatabase()`. In Ihrer Deepkit App geschieht dies jedoch automatisch mithilfe des Dependency-Injection-Containers. + +## Migration + +Das Deepkit Framework verfügt über ein leistungsfähiges Migration-System, mit dem Sie Migrationen erstellen, ausführen und zurücksetzen können. Das Migrationssystem basiert auf der Deepkit ORM Bibliothek und ist daher perfekt in das Framework integriert. + +Das `FrameworkModule` stellt mehrere Befehle zur Verwaltung von Migrationen bereit. + +- `migration:create` - Generiert eine neue Migrationsdatei basierend auf einem Datenbank-Diff +- `migration:pending` - Zeigt ausstehende Migrationsdateien +- `migration:up` - Führt ausstehende Migrationsdateien aus. +- `migration:down` - Führt Down-Migration aus und macht ältere Migrationsdateien rückgängig + + +```sh +ts-node app.ts migration:create --migrationDir src/migrations +``` + +Eine neue Migrationsdatei wird in `migrations` erstellt. Dieser Ordner ist das standardmäßig im FrameworkModule konfigurierte Verzeichnis. Um es zu ändern, passen Sie die Konfiguration entweder über Umgebungsvariablen an (wie im Kapitel [Konfiguration](configuration.md) beschrieben) oder indem Sie die Option `migrationDir` an den Konstruktor von `FrameworkModule` übergeben. + +```typescript +new FrameworkModule({ + migrationDir: 'src/migrations', +}) +``` + +Die neu erstellte Migrationsdatei enthält nun die up- und down-Methoden basierend auf dem Unterschied zwischen den in Ihrer TypeScript App definierten Entities und der konfigurierten Datenbank. +Sie können nun die up-Methode nach Ihren Bedürfnissen anpassen. Die down-Methode wird basierend auf der up-Methode automatisch generiert. +Committen Sie diese Datei in Ihr Repository, damit andere Entwickler sie ebenfalls ausführen können. + +### Ausstehende Migrationen + +```sh +ts-node app.ts migration:pending --migrationDir src/migrations +``` + +Dies zeigt alle ausstehenden Migrationen. Wenn Sie eine neue Migrationsdatei haben, die noch nicht ausgeführt wurde, wird sie hier aufgelistet. + +### Migrationen ausführen + +```sh +ts-node app.ts migration:up --migrationDir src/migrations +``` + +Dies führt die nächste ausstehende Migration aus. + +### Migrationen zurückrollen + +```sh +ts-node app.ts migration:down --migrationDir src/migrations +``` + +Dies macht die zuletzt ausgeführte Migration rückgängig. + +### Fake-Migrationen + +Angenommen, Sie wollten eine Migration (up oder down) ausführen, aber sie ist fehlgeschlagen. Sie haben das Problem manuell behoben, können die Migration nun jedoch nicht erneut ausführen, weil sie bereits ausgeführt wurde. Sie können die Option `--fake` verwenden, um die Migration zu faken, sodass sie in der Datenbank als ausgeführt markiert wird, ohne sie tatsächlich auszuführen. Auf diese Weise können Sie die nächste ausstehende Migration ausführen. + +```sh +ts-node app.ts migration:up --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/framework/deployment.md b/website/src/translations/de/documentation/framework/deployment.md new file mode 100644 index 000000000..389e17e51 --- /dev/null +++ b/website/src/translations/de/documentation/framework/deployment.md @@ -0,0 +1,138 @@ +# Bereitstellung + +In diesem Kapitel lernen Sie, wie Sie Ihre Anwendung nach JavaScript kompilieren, für Ihre Produktionsumgebung konfigurieren und mit Docker bereitstellen. + +## TypeScript kompilieren + +Angenommen, Sie haben eine Anwendung wie diese in einer `app.ts`-Datei: + +```typescript +#!/usr/bin/env ts-node-script +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class Config { + title: string = 'DEV my Page'; +} + +class MyWebsite { + constructor(protected title: Config['title']) { + } + + @http.GET() + helloWorld() { + return 'Hello from ' + this.title; + } +} + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + .loadConfigFromEnv() + .run(); +``` + +Wenn Sie `ts-node app.ts server:start` verwenden, sehen Sie, dass alles korrekt funktioniert. In einer Produktionsumgebung würden Sie den Server normalerweise nicht mit `ts-node` starten. Sie würden ihn nach JavaScript kompilieren und dann mit Node ausführen. Dazu benötigen Sie eine korrekte `tsconfig.json` mit den passenden Konfigurationsoptionen. Im Abschnitt „Erste Anwendung“ ist Ihre `tsconfig.json` so konfiguriert, dass JavaScript in den Ordner `.dist` ausgegeben wird. Wir gehen davon aus, dass Sie es ebenfalls so konfiguriert haben. + +Wenn alle Compiler-Einstellungen korrekt sind und Ihr `outDir` auf einen Ordner wie `dist` zeigt, werden, sobald Sie den Befehl `tsc` in Ihrem Projekt ausführen, alle in der `tsconfig.json` verlinkten Dateien nach JavaScript kompiliert. Es reicht aus, Ihre Entry-Dateien in dieser Liste anzugeben. Alle importierten Dateien werden ebenfalls automatisch kompiliert und müssen nicht explizit zur `tsconfig.json` hinzugefügt werden. `tsc` ist Teil von TypeScript, wenn Sie `npm install typescript` installieren. + +```sh +$ ./node_modules/.bin/tsc +``` + +Der TypeScript-Compiler gibt nichts aus, wenn er erfolgreich war. Sie können jetzt die Ausgabe in `dist` überprüfen. + +```sh +$ tree dist +dist +└── app.js +``` + +Sie sehen, dass es nur eine Datei gibt. Sie können sie über `node distapp.js` ausführen und erhalten dieselbe Funktionalität wie mit `ts-node app.ts`. + +Für ein Deployment ist es wichtig, dass die TypeScript-Dateien korrekt kompiliert werden und alles direkt über Node funktioniert. Sie könnten nun einfach Ihren `dist`-Ordner inklusive Ihrer `node_modules` verschieben und `node distapp.js server:start` ausführen, und Ihre App ist erfolgreich bereitgestellt. Allerdings würden Sie andere Lösungen wie Docker verwenden, um Ihre App korrekt zu paketieren. + +## Konfiguration + +In einer Produktionsumgebung würden Sie den Server nicht an `localhost` binden, sondern höchstwahrscheinlich an alle Geräte über `0.0.0.0`. Wenn Sie nicht hinter einem Reverse Proxy sind, würden Sie auch den Port auf 80 setzen. Um diese beiden Einstellungen zu konfigurieren, müssen Sie das `FrameworkModule` anpassen. Die beiden Optionen, die uns interessieren, sind `host` und `port`. Damit diese extern über Umgebungsvariablen oder über .dotenv-Dateien konfiguriert werden können, müssen wir dies zunächst erlauben. Glücklicherweise hat unser obiger Code dies bereits mit der Methode `loadConfigFromEnv()` erledigt. + +Weitere Informationen darüber, wie Sie die Anwendungs-Konfigurationsoptionen setzen, finden Sie im Kapitel [Konfiguration](../app/configuration.md). + +Um zu sehen, welche Konfigurationsoptionen verfügbar sind und welchen Wert sie haben, können Sie den Befehl `ts-node app.ts app:config` verwenden. Sie können sie auch im Framework Debugger sehen. + +### SSL + +Es wird empfohlen (und manchmal verlangt), Ihre Anwendung über HTTPS mit SSL zu betreiben. Es gibt mehrere Optionen zur Konfiguration von SSL. Um SSL zu aktivieren, verwenden Sie +`framework.ssl` und konfigurieren seine Parameter mit den folgenden Optionen. + +|=== +|Name|Typ|Beschreibung + +|framework.ssl|boolean|Aktiviert den HTTPS-Server, wenn true +|framework.httpsPort|number?|Wenn httpsPort und ssl definiert sind, wird der HTTPS-Server zusätzlich zum HTTP-Server gestartet. +|framework.sslKey|string?|Ein Dateipfad zu einer SSL-Key-Datei für HTTPS +|framework.sslCertificate|string?|Ein Dateipfad zu einer Zertifikatsdatei für HTTPS +|framework.sslCa|string?|Ein Dateipfad zu einer CA-Datei für HTTPS +|framework.sslCrl|string?|Ein Dateipfad zu einer CRL-Datei für HTTPS +|framework.sslOptions|object?|Gleiches Interface wie tls.SecureContextOptions & tls.TlsOptions. +|=== + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// deine Config und HTTP-Controller hier + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + sslKey: __dirname + 'path/ssl.key', + sslCertificate: __dirname + 'path/ssl.cert', + sslCA: __dirname + 'path/ssl.ca', + }) + ] +}) + .run(); +``` + +### Lokales SSL + +In der lokalen Entwicklungsumgebung können Sie selbstsigniertes HTTPS mit der Option `framework.selfSigned` aktivieren. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// deine Config und HTTP-Controller hier + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + }) + ] +}) + .run(); +``` + +```sh +$ ts-node app.ts server:start +2021-06-13T18:04:01.563Z [LOG] Start HTTP server, using 1 workers. +2021-06-13T18:04:01.598Z [LOG] Self signed certificate for localhost created at var/self-signed-localhost.cert +2021-06-13T18:04:01.598Z [LOG] Tip: If you want to open this server via chrome for localhost, use chrome://flags/#allow-insecure-localhost +2021-06-13T18:04:01.606Z [LOG] HTTP MyWebsite +2021-06-13T18:04:01.606Z [LOG] GET / helloWorld +2021-06-13T18:04:01.606Z [LOG] HTTPS listening at https://localhost:8080/ +``` + +Wenn Sie diesen Server jetzt starten, ist Ihr HTTP-Server als HTTPS unter `https:localhost:8080` erreichbar. In Chrome erhalten Sie nun die Fehlermeldung „NET::ERR_CERT_INVALID“, wenn Sie diese URL öffnen, da selbstsignierte Zertifikate als Sicherheitsrisiko gelten: `chrome:flagsallow-insecure-localhost`. \ No newline at end of file diff --git a/website/src/translations/de/documentation/framework/public.md b/website/src/translations/de/documentation/framework/public.md new file mode 100644 index 000000000..d156699a9 --- /dev/null +++ b/website/src/translations/de/documentation/framework/public.md @@ -0,0 +1,34 @@ +# Öffentliches Verzeichnis + +Das `FrameworkModule` bietet eine Möglichkeit, statische Dateien wie Bilder, PDFs, Binärdateien usw. über HTTP auszuliefern. Die Konfigurationsoption `publicDir` ermöglicht es Ihnen, anzugeben, welcher Ordner als Standard-Einstiegspunkt für Anfragen verwendet wird, die nicht zu einer HTTP-Controller-Route führen. Standardmäßig ist dieses Verhalten deaktiviert (leerer Wert). + +Um die Bereitstellung öffentlicher Dateien zu aktivieren, setzen Sie `publicDir` auf einen Ordner Ihrer Wahl. Üblicherweise wählt man einen Namen wie `publicDir`, um es eindeutig zu machen. + +``` +. +├── app.ts +└── publicDir + └── logo.jpg +``` + +Um die Option `publicDir` zu ändern, können Sie das erste Argument von `FrameworkModule` ändern. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// Ihre Config und HTTP Controller hier + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + publicDir: 'publicDir' + }) + ] +}) + .run(); +``` + +Alle Dateien in diesem konfigurierten Ordner sind nun über HTTP zugänglich. Wenn Sie zum Beispiel `http:localhost:8080/logo.jpg` aufrufen, sehen Sie das Bild `logo.jpg` im Verzeichnis `publicDir`. \ No newline at end of file diff --git a/website/src/translations/de/documentation/framework/testing.md b/website/src/translations/de/documentation/framework/testing.md new file mode 100644 index 000000000..db5b1999e --- /dev/null +++ b/website/src/translations/de/documentation/framework/testing.md @@ -0,0 +1,179 @@ +# Testen + +Die Services und Controller im Deepkit-Framework sind darauf ausgelegt, SOLID und Clean Code zu unterstützen, der gut entworfen, gekapselt und getrennt ist. Diese Eigenschaften machen den Code leicht testbar. + +Diese Dokumentation zeigt Ihnen, wie Sie ein Test-Framework namens [Jest](https://jestjs.io) mit `ts-jest` einrichten. Führen Sie dazu den folgenden Befehl aus, um `jest` und `ts-jest` zu installieren. + +```sh +npm install jest ts-jest @types/jest +``` + +Jest benötigt einige Konfigurationsoptionen, um zu wissen, wo die Test-Suites zu finden sind und wie der TS-Code kompiliert wird. Fügen Sie die folgende Konfiguration zu Ihrer `package.json` hinzu: + +```json title=package.json +{ + ..., + + "jest": { + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "testEnvironment": "node", + "testMatch": [ + "**/*.spec.ts" + ] + } +} +``` + +Ihre Testdateien sollten mit `.spec.ts` benannt werden. Erstellen Sie eine Datei `test.spec.ts` mit folgendem Inhalt. + +```typescript +test('first test', () => { + expect(1 + 1).toBe(2); +}); +``` + +Sie können nun den Befehl jest verwenden, um alle Ihre Test-Suites auf einmal auszuführen. + +```sh +$ node_modules/.bin/jest + PASS ./test.spec.ts + ✓ first test (1 ms) + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: 0.23 s, estimated 1 s +Ran all test suites. +``` + +Bitte lesen Sie die [Jest-Dokumentation](https://jestjs.io), um mehr darüber zu erfahren, wie das Jest-CLI-Tool funktioniert und wie Sie ausgefeiltere Tests und ganze Test-Suites schreiben können. + +## Unit-Test + +Wann immer möglich, sollten Sie Ihre Services mit Unit-Tests testen. Je einfacher, besser getrennt und besser definiert Ihre Service-Abhängigkeiten sind, desto leichter lassen sie sich testen. In diesem Fall können Sie einfache Tests wie die folgenden schreiben: + +```typescript +export class MyService { + helloWorld() { + return 'hello world'; + } +} +``` + +```typescript +// +import { MyService } from './my-service.ts'; + +test('hello world', () => { + const myService = new MyService(); + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +## Integrationstests + +Es ist nicht immer möglich, Unit-Tests zu schreiben, und es ist auch nicht immer der effizienteste Weg, um geschäftskritischen Code und Verhalten abzudecken. Besonders wenn Ihre Architektur sehr komplex ist, ist es vorteilhaft, End-to-End-Integrationstests einfach durchführen zu können. + +Wie Sie bereits im Kapitel Dependency Injection gelernt haben, ist der Dependency Injection Container das Herz von Deepkit. Hier werden alle Services erstellt und ausgeführt. Ihre Anwendung definiert Services (Providers), Controller, Listener und Imports. Für Integrationstests möchten Sie in einem Testfall nicht unbedingt alle Services verfügbar haben, sondern in der Regel eine abgespeckte Version der Anwendung, um die kritischen Bereiche zu testen. + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { http, HttpRequest } from '@deepkit/http'; + +test('http controller', async () => { + class MyController { + + @http.GET() + hello(@http.query() text: string) { + return 'hello ' + text; + } + } + + const testing = createTestingApp({ controllers: [MyController] }); + await testing.startServer(); + + const response = await testing.request(HttpRequest.GET('/').query({text: 'world'})); + + expect(response.getHeader('content-type')).toBe('text/plain; charset=utf-8'); + expect(response.body.toString()).toBe('hello world'); +}); +``` + +```typescript +import { createTestingApp } from '@deepkit/framework'; + +test('service', async () => { + class MyService { + helloWorld() { + return 'hello world'; + } + } + + const testing = createTestingApp({ providers: [MyService] }); + + // Zugriff auf den Dependency Injection Container und MyService instanziieren + const myService = testing.app.get(MyService); + + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +Wenn Sie Ihre Anwendung in mehrere Module aufgeteilt haben, können Sie diese leichter testen. Angenommen, Sie haben ein `AppCoreModule` erstellt und möchten einige Services testen. + +```typescript +class Config { + items: number = 10; +} + +export class MyService { + constructor(protected items: Config['items']) { + + } + + doIt(): boolean { + //etwas tun + return true; + } +} + +export AppCoreModule = new AppModule({}, { + config: config, + provides: [MyService] +}, 'core'); +``` + +Sie verwenden Ihr Modul wie folgt: + +```typescript +import { AppCoreModule } from './app-core.ts'; + +new App({ + imports: [new AppCoreModule] +}).run(); +``` + +Und testen Sie es, ohne den gesamten Anwendungsserver zu starten. + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { AppCoreModule, MyService } from './app-core.ts'; + +test('service simple', async () => { + const testing = createTestingApp({ imports: [new AppCoreModule] }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); + +test('service simple big', async () => { + // Sie ändern Konfigurationen Ihres Moduls für spezifische Testszenarien + const testing = createTestingApp({ + imports: [new AppCoreModule({items: 100})] + }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/http.md b/website/src/translations/de/documentation/http.md new file mode 100644 index 000000000..9a74b205c --- /dev/null +++ b/website/src/translations/de/documentation/http.md @@ -0,0 +1,69 @@ +# HTTP + +Die Verarbeitung von HTTP-Anfragen gehört zu den bekanntesten Aufgaben eines Servers. Dabei wird eine Eingabe (HTTP Request) in eine Ausgabe (HTTP Response) überführt und eine spezifische Aufgabe ausgeführt. Ein Client kann Daten auf unterschiedliche Weise per HTTP Request an den Server senden, die korrekt gelesen und verarbeitet werden müssen. Neben dem HTTP-Body sind auch HTTP-Query- oder HTTP-Header-Werte möglich. Wie Daten tatsächlich verarbeitet werden, hängt vom Server ab. Der Server definiert, wohin und wie die Werte vom Client gesendet werden sollen. + +Die höchste Priorität ist hier nicht nur, das zu tun, was der Benutzer erwartet, sondern alle Eingaben aus dem HTTP Request korrekt zu konvertieren (deserialisieren) und zu validieren. + +Die Pipeline, durch die ein HTTP Request auf dem Server läuft, kann vielfältig und komplex sein. Viele einfache HTTP-Libraries reichen für eine gegebene Route nur den HTTP Request und die HTTP Response durch und erwarten, dass der Entwickler die HTTP Response direkt verarbeitet. Eine Middleware-API ermöglicht es, die Pipeline bei Bedarf zu erweitern. + +_Express-Beispiel_ + +```typescript +const http = express(); +http.get('/user/:id', (request, response) => { + response.send({id: request.params.id, username: 'Peter' ); +}); +``` + +Das ist für einfache Use Cases sehr gut geeignet, wird jedoch mit wachsender Anwendung schnell unübersichtlich, da alle Eingaben und Ausgaben manuell serialisiert bzw. deserialisiert und validiert werden müssen. Außerdem muss bedacht werden, wie Objekte und Services wie eine Datenbankabstraktion aus der Anwendung selbst bezogen werden können. Es zwingt den Entwickler dazu, eine Architektur darüberzulegen, die diese obligatorischen Funktionalitäten abbildet. + +Deepkits HTTP Library nutzt hingegen die Möglichkeiten von TypeScript und Dependency Injection. Serialisierung/Deserialisierung und Validierung beliebiger Werte erfolgen automatisch auf Basis der definierten Types. Zudem können Routen entweder über eine funktionale API wie im obigen Beispiel oder über Controller-Klassen definiert werden, um die unterschiedlichen Anforderungen einer Architektur abzudecken. + +Es kann entweder mit einem bestehenden HTTP-Server wie Nodes `http`-Modul oder mit dem Deepkit-Framework verwendet werden. Beide API-Varianten haben Zugriff auf den Dependency-Injection-Container und können so bequem Objekte wie eine Datenbankabstraktion und Konfigurationen aus der Anwendung beziehen. + +## Beispiel: Funktionale API + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; + +//Funktionale API +const app = new App({ + imports: [new FrameworkModule()] +}); +const router = app.get(HttpRouterRegistry); + +router.get('/user/:id', (id: number & Positive, database: Database) => { + //id ist garantiert eine number und positiv. + //database wird vom DI Container injiziert. + return database.query(User).filter({ id }).findOne(); +}); + +app.run(); +``` + +## Controller-API mit Klassen + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; +import { User } from "discord.js"; + +//Controller-API +class UserController { + constructor(private database: Database) { + } + + @http.GET('/user/:id') + user(id: number & Positive) { + return this.database.query(User).filter({ id }).findOne(); + } +} + +const app = new App({ + controllers: [UserController], + imports: [new FrameworkModule()] +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/http/dependency-injection.md b/website/src/translations/de/documentation/http/dependency-injection.md new file mode 100644 index 000000000..84a3781cc --- /dev/null +++ b/website/src/translations/de/documentation/http/dependency-injection.md @@ -0,0 +1,67 @@ +# Dependency Injection + +Die Router-Funktionen sowie die Controller-Klassen und Controller-Methoden können beliebige Dependencies definieren, die vom Dependency Injection-Container aufgelöst werden. So ist es z. B. möglich, bequem auf eine Datenbankabstraktion oder einen Logger zuzugreifen. + +Wenn z. B. eine Datenbank als Provider bereitgestellt wurde, kann sie injiziert werden: + +```typescript +class Database { + //... +} + +const app = new App({ + providers: [ + Database, + ], +}); +``` + +_Funktionale API:_ + +```typescript +router.get('/user/:id', async (id: number, database: Database) => { + return await database.query(User).filter({id}).findOne(); +}); +``` + +_Controller-API:_ + +```typescript +class UserController { + constructor(private database: Database) {} + + @http.GET('/user/:id') + async userDetail(id: number) { + return await this.database.query(User).filter({id}).findOne(); + } +} + +//alternativ direkt in der Methode +class UserController { + @http.GET('/user/:id') + async userDetail(id: number, database: Database) { + return await database.query(User).filter({id}).findOne(); + } +} +``` + +Siehe [Dependency Injection](dependency-injection), um mehr zu erfahren. + +## Scope + +Alle HTTP-Controller und funktionalen Routen werden innerhalb des `http` Dependency Injection-Scope verwaltet. HTTP-Controller werden entsprechend für jede HTTP-Anfrage instanziiert. Das bedeutet auch, dass beide auf Provider zugreifen können, die für den `http` Scope registriert sind. Zusätzlich sind `HttpRequest` und `HttpResponse` aus `@deepkit/http` als Dependencies nutzbar. Wenn das deepkit framework verwendet wird, ist auch `SessionHandler` aus `@deepkit/framework` verfügbar. + +```typescript +import { HttpResponse } from '@deepkit/http'; + +router.get('/user/:id', (id: number, request: HttpRequest) => { +}); + +router.get('/', (response: HttpResponse) => { + response.end('Hello'); +}); +``` + +Es kann sinnvoll sein, Provider im `http` Scope zu platzieren, z. B. um Services für jede HTTP-Anfrage zu instanziieren. Sobald die HTTP-Anfrage verarbeitet wurde, wird der DI-Container im `http` Scope gelöscht, wodurch alle seine Provider-Instanzen vom Garbage Collector (GC) bereinigt werden. + +Siehe [Dependency Injection Scopes](dependency-injection.md#di-scopes), um zu erfahren, wie Provider im `http` Scope platziert werden. \ No newline at end of file diff --git a/website/src/translations/de/documentation/http/events.md b/website/src/translations/de/documentation/http/events.md new file mode 100644 index 000000000..3153635bf --- /dev/null +++ b/website/src/translations/de/documentation/http/events.md @@ -0,0 +1,84 @@ +# Ereignisse + +Das HTTP-Modul basiert auf einer Workflow-Engine, die verschiedene Event-Token bereitstellt, mit denen in den gesamten Prozess der Verarbeitung einer HTTP-Anfrage eingehakt werden kann. + +Die Workflow-Engine ist eine Finite State Machine (FSM), die für jede HTTP-Anfrage eine neue State-Machine-Instanz erstellt und dann von Position zu Position springt. Die erste Position ist `start` und die letzte `response`. In jeder Position kann zusätzlicher Code ausgeführt werden. + +![HTTP-Workflow](/assets/documentation/framework/http-workflow.png) + +Jedes Event-Token hat seinen eigenen Event-Typ mit zusätzlichen Informationen. + +| Event-Token | Beschreibung | +|-------------------------------|----------------------------------------------------------------------------------------------------------------------| +| httpWorkflow.onRequest | Wenn eine neue Anfrage eingeht | +| httpWorkflow.onRoute | Wenn die Route aus der Anfrage aufgelöst werden soll | +| httpWorkflow.onRouteNotFound | Wenn die Route nicht gefunden wird | +| httpWorkflow.onAuth | Wenn die Authentifizierung stattfindet | +| httpWorkflow.onResolveParameters | Wenn die Routenparameter aufgelöst werden | +| httpWorkflow.onAccessDenied | Wenn der Zugriff verweigert wird | +| httpWorkflow.onController | Wenn die Controller-Action aufgerufen wird | +| httpWorkflow.onControllerError | Wenn die Controller-Action einen Fehler ausgelöst hat | +| httpWorkflow.onParametersFailed | Wenn das Auflösen der Routenparameter fehlgeschlagen ist | +| httpWorkflow.onResponse | Wenn die Controller-Action aufgerufen wurde. Hier wird das Ergebnis in eine Response umgewandelt. | + +Da alle HTTP-Events auf der Workflow-Engine basieren, kann ihr Verhalten geändert werden, indem das angegebene Event verwendet und mit der Methode `event.next()` dorthin gesprungen wird. + +Das HTTP-Modul verwendet eigene Event-Listener auf diesen Event-Token, um die Verarbeitung von HTTP-Anfragen zu implementieren. Alle diese Event-Listener haben eine Priorität von 100. Das bedeutet, dass Ihr Listener standardmäßig zuerst ausgeführt wird, wenn Sie auf ein Event hören (da die Standardpriorität 0 ist). Fügen Sie eine Priorität über 100 hinzu, um nach dem HTTP-Standardhandler zu laufen. + +Angenommen, Sie möchten das Event abfangen, wenn ein Controller aufgerufen wird. Soll ein bestimmter Controller aufgerufen werden, prüfen wir, ob der Benutzer Zugriff darauf hat. Hat der Benutzer Zugriff, machen wir weiter. Wenn nicht, springen wir zum nächsten Workflow-Item `accessDenied`. Dort wird die Prozedur einer Zugriff-verweigert-Situation dann automatisch weiterverarbeitet. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HtmlResponse, http, httpAction, httpWorkflow } from '@deepkit/http'; +import { eventDispatcher } from '@deepkit/event'; + +class MyWebsite { + @http.GET('/') + open() { + return 'Welcome'; + } + + @http.GET('/admin').group('secret') + secret() { + return 'Welcome to the dark side'; + } +} + +const app = new App({ + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + +app.listen(httpWorkflow.onController, async (event) => { + if (event.route.groups.includes('secret')) { + //prüfen Sie hier Authentifizierungsinformationen wie Cookie-Session, JWT etc. + + //dies springt in den 'accessDenied'-Workflow-Zustand, + // und führt im Grunde alle onAccessDenied-Listener aus. + + //da unser Listener vor dem des HTTP-Kernels aufgerufen wird, + // wird die Standard-Controller-Action niemals aufgerufen. + //das ruft unter der Haube event.next('accessDenied', ...) auf + event.accessDenied(); + } +}); + +/** + * Wir ändern die Standardimplementierung von accessDenied. + */ +app.listen(httpWorkflow.onAccessDenied, async () => { + if (event.sent) return; + if (event.hasNext()) return; + event.send(new HtmlResponse('No access to this area.', 403)); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Welcome +$ curl http://localhost:8080/admin +No access to this area +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/http/getting-started.md b/website/src/translations/de/documentation/http/getting-started.md new file mode 100644 index 000000000..9149ab95c --- /dev/null +++ b/website/src/translations/de/documentation/http/getting-started.md @@ -0,0 +1,210 @@ +# Erste Schritte + +Da Deepkit HTTP auf Runtime Types basiert, ist es notwendig, dass Runtime Types bereits korrekt installiert sind. Siehe [Installation von Runtime Types](../runtime-types/getting-started.md). + +Wenn dies erfolgreich erfolgt ist, kann `@deepkit/app` installiert werden oder das Deepkit Framework, das die Library bereits unter der Haube verwendet. + +```sh +npm install @deepkit/http +``` + +Beachte, dass `@deepkit/http` für die Controller-API auf TypeScript-Annotations basiert und dieses Feature beim Einsatz der Controller-API mit `experimentalDecorators` aktiviert werden muss. +Wenn du keine Klassen verwendest, musst du dieses Feature nicht aktivieren. + +_Datei: tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +Sobald die Library installiert ist, kann ihre API direkt verwendet werden. + +## Funktionale API + +Die funktionale API basiert auf Funktionen und kann über das Router-Registry registriert werden, die über den DI-Container der App bezogen werden kann. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule] +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return "Hello World!"; +}); + +app.run(); +``` + +Sobald Module verwendet werden, können funktionale Routen auch dynamisch von Modulen bereitgestellt werden. + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +class MyModule extends createModuleClass({}) { + override process() { + this.configureProvider(router => { + router.get('/', () => { + return "Hello World!"; + }); + }); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +Siehe [Framework-Module](../app/modules), um mehr über App-Module zu erfahren. + +## Controller-API + +Die Controller-API basiert auf Klassen und kann über die App-API unter der Option `controllers` registriert werden. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +new App({ + controllers: [MyPage], + imports: [new FrameworkModule] +}).run(); +``` + +Sobald Module verwendet werden, können Controller auch durch Module bereitgestellt werden. + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +class MyModule extends createModuleClass({}) { + override process() { + this.addController(MyPage); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +Um Controller dynamisch bereitzustellen (z. B. abhängig von der Konfigurationsoption), kann der `process`-Hook verwendet werden. + +```typescript +class MyModuleConfiguration { + debug: boolean = false; +} + +class MyModule extends createModuleClass({ + config: MyModuleConfiguration +}) { + override process() { + if (this.config.debug) { + class DebugController { + @http.GET('/debug/') + root() { + return 'Hello Debugger'; + } + } + this.addController(DebugController); + } + } +} +``` + +Siehe [Framework-Module](../app/modules), um mehr über App-Module zu erfahren. + +## HTTP-Server + +Wenn das Deepkit Framework verwendet wird, ist ein HTTP-Server bereits integriert. Die HTTP-Library kann jedoch auch mit einem eigenen HTTP-Server ohne das Deepkit Framework verwendet werden. + +```typescript +import { Server } from 'http'; +import { HttpRequest, HttpResponse } from '@deepkit/http'; + +const app = new App({ + controllers: [MyPage], + imports: [new HttpModule] +}); + +const httpKernel = app.get(HttpKernel); + +new Server( + { IncomingMessage: HttpRequest, ServerResponse: HttpResponse, }, + ((req, res) => { + httpKernel.handleRequest(req as HttpRequest, res as HttpResponse); + }) +).listen(8080, () => { + console.log('listen at 8080'); +}); +``` + +## HTTP-Client + +todo: fetch API, validation, und cast. + +## Routen-Namen + +Routen können einen eindeutigen Namen erhalten, auf den bei der Weiterleitung verwiesen werden kann. Je nach API unterscheidet sich die Art, wie ein Name definiert wird. + +```typescript +//Funktionale API +router.get({ + path: '/user/:id', + name: 'userDetail' +}, (id: number) => { + return {userId: id}; +}); + +//Controller-API +class UserController { + @http.GET('/user/:id').name('userDetail') + userDetail(id: number) { + return {userId: id}; + } +} +``` + +Für alle Routen mit einem Namen kann die URL über `Router.resolveUrl()` abgefragt werden. + +```typescript +import { HttpRouter } from '@deepkit/http'; +const router = app.get(HttpRouter); +router.resolveUrl('userDetail', {id: 2}); //=> '/user/2' +``` + +## Sicherheit + +## Sitzungen \ No newline at end of file diff --git a/website/src/translations/de/documentation/http/input-output.md b/website/src/translations/de/documentation/http/input-output.md new file mode 100644 index 000000000..b13b7da00 --- /dev/null +++ b/website/src/translations/de/documentation/http/input-output.md @@ -0,0 +1,484 @@ +# Eingabe & Ausgabe + +Die Eingabe und Ausgabe einer HTTP-Route sind die Daten, die an den Server gesendet werden und die Daten, die an den Client zurückgesendet werden. Dazu gehören die Path-Parameter, Query-Parameter, der Body, Header und die Response selbst. In diesem Kapitel betrachten wir, wie man Daten in einer HTTP-Route liest, deserialisiert, validiert und schreibt. + +## Input + +Alle folgenden Eingabevarianten funktionieren sowohl für die funktionale API als auch für die Controller-API identisch. Sie ermöglichen es, Daten typsicher und entkoppelt aus einer HTTP-Request zu lesen. Das führt nicht nur zu deutlich erhöhter Sicherheit, sondern vereinfacht auch Unit-Tests, da streng genommen nicht einmal ein HTTP-Request-Objekt existieren muss, um die Route zu testen. + +Alle Parameter werden automatisch in den definierten TypeScript Type konvertiert (deserialisiert) und validiert. Dies geschieht über Deepkit Runtime Types und dessen Features [Serialisierung](../runtime-types/serialization.md) und [Validierung](../runtime-types/validation). + +Der Einfachheit halber sind unten alle Beispiele mit der funktionalen API gezeigt. + +### Path-Parameter + +Path-Parameter sind Werte, die aus der URL der Route extrahiert werden. Der Type des Werts hängt vom Type am zugehörigen Parameter der Function oder Method ab. Die Konvertierung erfolgt automatisch mit dem Feature [Soft Type Conversion](../runtime-types/serialization#soft-type-conversion). + +```typescript +router.get('/:text', (text: string) => { + return 'Hello ' + text; +}); +``` + +```sh +$ curl http://localhost:8080/galaxy +Hello galaxy +``` + +Wenn ein Path-Parameter als ein anderer Type als string definiert ist, wird er korrekt konvertiert. + +```typescript +router.get('/user/:id', (id: number) => { + return `${id} ${typeof id}`; +}); +``` + +```sh +$ curl http://localhost:8080/user/23 +23 number +``` + +Zusätzliche Validierungs-Constraints können ebenfalls auf die Types angewendet werden. + +```typescript +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: number & Positive) => { + return `${id} ${typeof id}`; +}); +``` + +Alle Validation Types aus `@deepkit/type` können angewendet werden. Mehr dazu siehe [HTTP-Validierung](#validation). + +Die Path-Parameter haben standardmäßig `[^]+` als regulären Ausdruck im URL-Matching gesetzt. Die RegExp kann dafür wie folgt angepasst werden: + +```typescript +import { HttpRegExp } from '@deepkit/http'; +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: HttpRegExp) => { + return `${id} ${typeof id}`; +}); +``` + +Dies ist nur in Ausnahmefällen notwendig, da häufig die Types in Kombination mit Validation Types bereits mögliche Werte korrekt einschränken. + +### Query-Parameter + +Query-Parameter sind Werte aus der URL nach dem Zeichen `?` und können mit dem Type `HttpQuery` gelesen werden. Der Name des Parameters entspricht dem Namen des Query-Parameters. + +```typescript +import { HttpQuery } from '@deepkit/http'; + +router.get('/', (text: HttpQuery) => { + return `Hello ${text}`; +}); +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +``` + +Query-Parameter werden ebenfalls automatisch deserialisiert und validiert. + +```typescript +import { HttpQuery } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/', (text: HttpQuery & MinLength<3>) => { + return 'Hello ' + text; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +$ curl http://localhost:8080/\?text\=ga +error +``` + +Alle Validation Types aus `@deepkit/type` können angewendet werden. Mehr dazu siehe [HTTP-Validierung](#validation). + +Warnung: Parameterwerte sind nicht escaped/sanitized. Deren direkte Rückgabe in einem String in einer Route als HTML eröffnet eine Sicherheitslücke (XSS). Stelle sicher, dass externen Eingaben niemals vertraut wird und filtere/sanitisieren/konvertiere Daten wo notwendig. + +### Query-Modell + +Bei einer großen Anzahl von Query-Parametern kann es schnell unübersichtlich werden. Um wieder Ordnung zu schaffen, kann ein Modell (Class oder Interface) verwendet werden, das alle möglichen Query-Parameter zusammenfasst. + +```typescript +import { HttpQueries } from '@deepkit/http'; + +class HelloWorldQuery { + text!: string; + page: number = 0; +} + +router.get('/', (query: HttpQueries) +{ + return 'Hello ' + query.text + ' at page ' + query.page; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy&page=1 +Hello galaxy at page 1 +``` + +Die Properties im angegebenen Modell können alle TypeScript Types und Validation Types enthalten, die `@deepkit/type` unterstützt. Siehe die Kapitel [Serialisierung](../runtime-types/serialization.md) und [Validierung](../runtime-types/validation.md). + +### Body + +Für HTTP-Methoden, die einen HTTP-Body erlauben, kann ebenfalls ein Body-Modell angegeben werden. Der Body Content-Type des HTTP-Requests muss entweder `application/x-www-form-urlencoded`, `multipart/form-data` oder `application/json` sein, damit Deepkit dies automatisch in JavaScript-Objekte konvertieren kann. + +```typescript +import { HttpBody } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBody) => { + return 'Hello ' + body.text; +} +``` + +### Header + +### Stream + +### Manuelles Validierungs-Handling + +Um die Validierung des Body-Modells manuell zu übernehmen, kann ein spezieller Type `HttpBodyValidation` verwendet werden. Er ermöglicht es, auch invalide Body-Daten zu empfangen und sehr spezifisch auf Fehlermeldungen zu reagieren. + +```typescript +import { HttpBodyValidation } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBodyValidation) => { + if (!body.valid()) { + // Houston, wir haben einige Fehler. + const textError = body.getErrorMessageForPath('text'); + return 'Text is invalid, please fix it. ' + textError; + } + + return 'Hello ' + body.text; +}) +``` + +Sobald `valid()` `false` zurückgibt, können die Werte im angegebenen Modell in einem fehlerhaften Zustand sein. Das bedeutet, dass die Validierung fehlgeschlagen ist. Wenn `HttpBodyValidation` nicht verwendet wird und ein fehlerhafter HTTP-Request empfangen wird, würde die Request direkt abgebrochen und der Code in der Function würde niemals ausgeführt werden. Verwende `HttpBodyValidation` nur, wenn z. B. Fehlermeldungen bezüglich des Bodys in derselben Route manuell verarbeitet werden sollen. + +Die Properties im angegebenen Modell können alle TypeScript Types und Validation Types enthalten, die `@deepkit/type` unterstützt. Siehe die Kapitel [Serialisierung](../runtime-types/serialization.md) und [Validierung](../runtime-types/validation.md). + +### Datei-Upload + +Ein spezieller Property Type im Body-Modell kann verwendet werden, um dem Client das Hochladen von Dateien zu erlauben. Es können beliebig viele `UploadedFile` verwendet werden. + +```typescript +import { UploadedFile, HttpBody } from '@deepkit/http'; +import { readFileSync } from 'fs'; + +class HelloWordBody { + file!: UploadedFile; +} + +router.post('/', (body: HttpBody) => { + const content = readFileSync(body.file.path); + + return { + uploadedFile: body.file + }; +}) +``` + +```sh +$ curl http://localhost:8080/ -X POST -H "Content-Type: multipart/form-data" -F "file=@Downloads/23931.png" +{ + "uploadedFile": { + "size":6430, + "path":"/var/folders/pn/40jxd3dj0fg957gqv_nhz5dw0000gn/T/upload_dd0c7241133326bf6afddc233e34affa", + "name":"23931.png", + "type":"image/png", + "lastModifiedDate":"2021-06-11T19:19:14.775Z" + } +} +``` + +Standardmäßig speichert der Router alle hochgeladenen Dateien in einem temporären Ordner und entfernt sie, sobald der Code in der Route ausgeführt wurde. Es ist daher notwendig, die Datei im angegebenen Pfad in `path` zu lesen und an einem dauerhaften Ort (lokale Festplatte, Cloud-Speicher, Datenbank) zu speichern. + +## Validierung + +Validierung in einem HTTP-Server ist eine zwingend notwendige Funktionalität, denn fast immer wird mit nicht vertrauenswürdigen Daten gearbeitet. Je mehr Stellen Daten validiert werden, desto stabiler ist der Server. Validierung in HTTP-Routen kann bequem über Types und Validierungs-Constraints genutzt werden und wird mit einem hochoptimierten Validator aus `@deepkit/type` geprüft, sodass es diesbezüglich keine Performance-Probleme gibt. Es wird daher dringend empfohlen, diese Validierungsmöglichkeiten zu nutzen. Lieber einmal zu viel als einmal zu wenig. + +Alle Inputs wie Path-Parameter, Query-Parameter und Body-Parameter werden automatisch für den angegebenen TypeScript Type validiert. Wenn zusätzliche Constraints über Types von `@deepkit/type` angegeben sind, werden diese ebenfalls geprüft. + +```typescript +import { HttpQuery, HttpQueries, HttpBody } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/:text', (text: string & MinLength<3>) => { + return 'Hello ' + text; +} + +router.get('/', (text: HttpQuery & MinLength<3>) => { + return 'Hello ' + text; +} + +interface MyQuery { + text: string & MinLength<3>; +} + +router.get('/', (query: HttpQueries) => { + return 'Hello ' + query.text; +}); + +router.post('/', (body: HttpBody) => { + return 'Hello ' + body.text; +}); +``` + +Siehe [Validierung](../runtime-types/validation.md) für weitere Informationen dazu. + +## Ausgabe + +Eine Route kann verschiedene Datenstrukturen zurückgeben. Einige davon werden speziell behandelt, wie Redirects und Templates, und andere, wie einfache Objekte, werden einfach als JSON gesendet. + +### JSON + +Standardmäßig werden normale JavaScript-Werte als JSON mit dem Header `applicationjson; charset=utf-8` an den Client zurückgegeben. + +```typescript +router.get('/', () => { + // wird als application/json gesendet + return { hello: 'world' } +}); +``` + +Wenn für die Function oder Method ein expliziter Return Type angegeben ist, werden die Daten mit dem Deepkit JSON Serializer gemäß diesem Type zu JSON serialisiert. + +```typescript +interface ResultType { + hello: string; +} + +router.get('/', (): ResultType => { + // wird als application/json gesendet und additionalProperty wird entfernt + return { hello: 'world', additionalProperty: 'value' }; +}); +``` + +### HTML + +Zum Senden von HTML gibt es zwei Möglichkeiten. Entweder wird das Objekt `HtmlResponse` oder die Template-Engine mit JSX verwendet. + +```typescript +import { HtmlResponse } from '@deepkit/http'; + +router.get('/', () => { + // wird mit Content-Type: text/html gesendet + return new HtmlResponse('Hello World'); +}); +``` + +```typescript +router.get('/', () => { + // wird mit Content-Type: text/html gesendet + return Hello + World < /b>; +}); +``` + +Die Template-Engine-Variante mit JSX hat den Vorteil, dass verwendete Variablen automatisch HTML-escaped werden. Siehe auch [Template](./template.md). + +### Benutzerdefinierter Content-Type + +Neben HTML und JSON ist es auch möglich, Text- oder Binärdaten mit einem bestimmten Content-Type zu senden. Dies geschieht über das Objekt `Response`. + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('Hello World', 'text/xml'); +}); +``` + +### HTTP-Fehler + +Durch Werfen verschiedener HTTP-Fehler ist es möglich, die Verarbeitung eines HTTP-Requests sofort zu unterbrechen und den entsprechenden HTTP-Status des Fehlers auszugeben. + +```typescript +import { HttpNotFoundError } from '@deepkit/http'; + +router.get('/user/:id', async (id: number, database: Database) => { + const user = await database.query(User).filter({ id }).findOneOrUndefined(); + if (!user) throw new HttpNotFoundError('User not found'); + return user; +}); +``` + +Standardmäßig werden alle Fehler als JSON an den Client zurückgegeben. Dieses Verhalten kann im Event-System unter dem Event `httpWorkflow.onControllerError` angepasst werden. Siehe den Abschnitt [HTTP-Events](./events.md). + +| Error Class | Status | +|---------------------------|--------| +| HttpBadRequestError | 400 | +| HttpUnauthorizedError | 401 | +| HttpAccessDeniedError | 403 | +| HttpNotFoundError | 404 | +| HttpMethodNotAllowedError | 405 | +| HttpNotAcceptableError | 406 | +| HttpTimeoutError | 408 | +| HttpConflictError | 409 | +| HttpGoneError | 410 | +| HttpTooManyRequestsError | 429 | +| HttpInternalServerError | 500 | +| HttpNotImplementedError | 501 | + +Der Fehler `HttpAccessDeniedError` ist ein Spezialfall. Sobald er geworfen wird, springt der HTTP-Workflow (siehe [HTTP-Events](./events.md)) nicht zu `controllerError`, sondern zu `accessDenied`. + +Eigene HTTP-Fehler können mit `createHttpError` erstellt und geworfen werden. + +```typescript +export class HttpMyError extends createHttpError(412, 'My Error Message') { +} +``` + +Geworfene Fehler in einer Controller-Action werden vom HTTP-Workflow-Event `onControllerError` behandelt. Die Standardimplementierung besteht darin, eine JSON-Response mit der Fehlermeldung und dem Statuscode zurückzugeben. Dies kann angepasst werden, indem man auf dieses Event lauscht und eine andere Response zurückgibt. + +```typescript +import { httpWorkflow } from '@deepkit/http'; + +new App() + .listen(httpWorkflow.onControllerError, (event) => { + if (event.error instanceof HttpMyError) { + event.send(new Response('My Error Message', 'text/plain').status(500)); + } else { + // für alle anderen Fehler eine generische Fehlermeldung zurückgeben + event.send(new Response('Something went wrong. Sorry about that.', 'text/plain').status(500)); + } + }) + .listen(httpWorkflow.onAccessDenied, (event) => { + event.send(new Response('Access denied. Try to login first.', 'text/plain').status(403)); + }); +``` + +### Zusätzliche Header + +Um den Header einer HTTP-Response zu ändern, können zusätzliche Methods an den Objekten `Response`, `JSONResponse` und `HTMLResponse` aufgerufen werden. + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('Access Denied', 'text/plain') + .header('X-Reason', 'unknown') + .status(403); +}); +``` + +### Redirect + +Um eine 301- oder 302-Weiterleitung als Response zurückzugeben, können `Redirect.toRoute` oder `Redirect.toUrl` verwendet werden. + +```typescript +import { Redirect } from '@deepkit/http'; + +router.get({ path: '/', name: 'homepage' }, () => { + return Hello + World < /b>; +}); + +router.get({ path: '/registration/complete' }, () => { + return Redirect.toRoute('homepage'); +}); +``` + +Die Method `Redirect.toRoute` verwendet hier den Routenname. Wie man einen Routenname setzt, ist im Abschnitt [HTTP-Routennamen](./getting-started.md#route-names) zu sehen. Wenn diese referenzierte Route (Query oder Path) Parameter enthält, können diese über das zweite Argument angegeben werden: + +```typescript +router.get({ path: '/user/:id', name: 'user_detail' }, (id: number) => { + +}); + +router.post('/user', (user: HttpBody) => { + //... store user and redirect to its detail page + return Redirect.toRoute('user_detail', { id: 23 }); +}); +``` + +Alternativ kann mit `Redirect.toUrl` auf eine URL weitergeleitet werden. + +```typescript +router.post('/user', (user: HttpBody) => { + //... store user and redirect to its detail page + return Redirect.toUrl('/user/' + 23); +}); +``` + +Standardmäßig verwenden beide eine 302-Weiterleitung. Dies kann über das Argument `statusCode` angepasst werden. + +## Resolver + +Der Router unterstützt eine Möglichkeit, komplexe Parameter Types zu resolven. Beispiel: Bei einer Route wie `/user/:id` kann diese `id` mittels eines Resolvers außerhalb der Route zu einem `user`-Objekt aufgelöst werden. Das entkoppelt die HTTP-Abstraktion und den Routen-Code weiter und vereinfacht Tests und Modularität zusätzlich. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http, RouteParameterResolverContext, RouteParameterResolver } from '@deepkit/http'; + +class UserResolver implements RouteParameterResolver { + constructor(protected database: Database) { + } + + async resolve(context: RouteParameterResolverContext) { + if (!context.parameters.id) throw new Error('No :id given'); + return await this.database.getUser(parseInt(context.parameters.id, 10)); + } +} + +@http.resolveParameter(User, UserResolver) +class MyWebsite { + @http.GET('/user/:id') + getUser(user: User) { + return 'Hello ' + user.username; + } +} + +new App({ + controllers: [MyWebsite], + providers: [UserDatabase, UserResolver], + imports: [new FrameworkModule] +}) + .run(); +``` + +Der Decorator in `@http.resolveParameter` gibt an, welche Class mit dem `UserResolver` aufzulösen ist. Sobald die angegebene Class `User` als Parameter in der Function oder Method angegeben ist, wird der Resolver verwendet, um sie bereitzustellen. + +Wenn `@http.resolveParameter` an der Class angegeben ist, erhalten alle Methods dieser Class diesen Resolver. Der Decorator kann auch pro Method angewendet werden: + +```typescript +class MyWebsite { + @http.GET('/user/:id').resolveParameter(User, UserResolver) + getUser(user: User) { + return 'Hello ' + user.username; + } +} +``` + +Auch die funktionale API kann verwendet werden: + +```typescript + +router.add( + http.GET('/user/:id').resolveParameter(User, UserResolver), + (user: User) => { + return 'Hello ' + user.username; + } +); +``` + +Das `User`-Objekt muss nicht zwingend von einem Parameter abhängen. Es könnte ebenso von einer Session oder einem HTTP-Header abhängen und nur bereitgestellt werden, wenn der Benutzer eingeloggt ist. In `RouteParameterResolverContext` stehen viele Informationen über den HTTP-Request zur Verfügung, sodass viele Anwendungsfälle abgebildet werden können. + +Grundsätzlich ist es auch möglich, komplexe Parameter Types über den Dependency Injection-Container aus dem `http`-Scope bereitstellen zu lassen, da diese ebenfalls in der Routen-Function oder -Method verfügbar sind. Dies hat jedoch den Nachteil, dass keine asynchronen Function-Aufrufe verwendet werden können, da der DI-Container durchgängig synchron ist. \ No newline at end of file diff --git a/website/src/translations/de/documentation/http/middleware.md b/website/src/translations/de/documentation/http/middleware.md new file mode 100644 index 000000000..0361b8167 --- /dev/null +++ b/website/src/translations/de/documentation/http/middleware.md @@ -0,0 +1,261 @@ +# Middleware + +HTTP-Middlewares ermöglichen es, sich als Alternative zu HTTP-Events in den Request/Response-Zyklus einzuklinken. Die API erlaubt die Nutzung aller Middlewares aus dem Express/Connect-Framework. + +Eine Middleware kann entweder eine Class (die vom Dependency-Injection-Container instanziiert wird) oder eine einfache Function sein. + +```typescript +import { HttpMiddleware, httpMiddleware, HttpRequest, HttpResponse } from '@deepkit/http'; + +class MyMiddleware implements HttpMiddleware { + async execute(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); + } +} + + +function myMiddlewareFunction(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); +} + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware), + httpMiddleware.for(myMiddlewareFunction), + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Global + +Mit httpMiddleware.for(MyMiddleware) wird eine Middleware global für alle Routes registriert. + +```typescript +import { httpMiddleware } from '@deepkit/http'; + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Pro Controller + +Sie können Middlewares auf einen oder mehrere Controller auf zwei Arten beschränken: Entweder mit `@http.controller` oder `httpMiddleware.for(T).forControllers()`. `excludeControllers` erlaubt es, Controller auszuschließen. + +```typescript +@http.middleware(MyMiddleware) +class MyFirstController { + +} +new App({ + providers: [MyMiddleware], + controllers: [MainController, UsersCommand], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyFirstController, MySecondController) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Pro Route-Name + +`forRouteNames` zusammen mit dem Gegenstück `excludeRouteNames` ermöglicht es, die Ausführung einer Middleware pro Route-Namen zu filtern. + +```typescript +class MyFirstController { + @http.GET('/hello').name('firstRoute') + myAction() { + } + + @http.GET('/second').name('secondRoute') + myAction2() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRouteNames('firstRoute', 'secondRoute') + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Pro Action/Route + +Um eine Middleware nur für eine bestimmte Route auszuführen, können Sie entweder `@http.GET().middleware()` oder +`httpMiddleware.for(T).forRoute()` verwenden, wobei forRoute mehrere Optionen bietet, um Routes zu filtern. + +```typescript +class MyFirstController { + @http.GET('/hello').middleware(MyMiddleware) + myAction() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' + }) + ], + imports: [new FrameworkModule] +}).run(); +``` + +`forRoutes()` erlaubt als erstes Argument mehrere Möglichkeiten, nach Routes zu filtern. + +```typescript +{ + path?: string; + pathRegExp?: RegExp; + httpMethod?: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS' | 'TRACE'; + category?: string; + excludeCategory?: string; + group?: string; + excludeGroup?: string; +} +``` + +## Pfad-Muster + +`path` unterstützt Wildcard *. + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' +}) +``` + +## RegExp + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + pathRegExp: /'api/.*'/ +}) +``` + +## HTTP-Methode + +Alle Routes nach einer HTTP-Methode filtern. + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + httpMethod: 'GET' +}) +``` + +## Kategorie + +`category` zusammen mit dem Gegenstück `excludeCategory` ermöglicht das Filtern nach Route-Kategorie. + +```typescript +@http.category('myCategory') +class MyFirstController { + +} + +class MySecondController { + @http.GET().category('myCategory') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + category: 'myCategory' +}) +``` + +## Gruppe + +`group` zusammen mit dem Gegenstück `excludeGroup` ermöglicht das Filtern nach Route-Gruppe. + +```typescript +@http.group('myGroup') +class MyFirstController { + +} + +class MySecondController { + @http.GET().group('myGroup') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + group: 'myGroup' +}) +``` + +## Pro Module + +Sie können die Ausführung einer Middleware auf ein ganzes Modul begrenzen. + +```typescript +httpMiddleware.for(MyMiddleware).forModule(ApiModule) +``` + +## Für eigene Module + +Um eine Middleware für alle Controller/Routes eines Moduls auszuführen, in dem die Middleware registriert wurde, verwenden Sie `forSelfModules()`. + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //für alle im selben Modul registrierten Controller + httpMiddleware.for(MyMiddleware).forSelfModules(), + ], +}); +``` + +## Timeout + +Alle Middlewares müssen früher oder später `next()` ausführen. Wenn eine Middleware `next()` innerhalb eines Timeouts nicht ausführt, wird eine Warnung geloggt und die nächste Middleware ausgeführt. Um den Standard von 4 Sekunden zu ändern, verwenden Sie `timeout(milliseconds)`. + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //für alle im selben Modul registrierten Controller + httpMiddleware.for(MyMiddleware).timeout(15_000), + ], +}); +``` + +## Mehrere Regeln + +Um mehrere Filter zu kombinieren, können Sie Methodenaufrufe verketten. + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MyController], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyController).excludeRouteNames('secondRoute') + ], +}); +``` + +## Express-Middleware + +Fast alle Express-Middlewares werden unterstützt. Solche, die auf bestimmte Request-Methoden von Express zugreifen, werden noch nicht unterstützt. + +```typescript +import * as compression from 'compression'; + +const ApiModule = new AppModule({}, { + middlewares: [ + httpMiddleware.for(compress()).forControllers(MyController) + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/http/security.md b/website/src/translations/de/documentation/http/security.md new file mode 100644 index 000000000..540a519fe --- /dev/null +++ b/website/src/translations/de/documentation/http/security.md @@ -0,0 +1,3 @@ +# Sicherheit + +Dieser Abschnitt befindet sich noch im Aufbau. \ No newline at end of file diff --git a/website/src/translations/de/documentation/http/views.md b/website/src/translations/de/documentation/http/views.md new file mode 100644 index 000000000..8adc3189e --- /dev/null +++ b/website/src/translations/de/documentation/http/views.md @@ -0,0 +1,31 @@ +# HTML-Views + +Deepkit HTTP kommt mit einem integrierten HTML-View-Rendering-System. Es basiert auf JSX und ermöglicht es, Views in TypeScript zu schreiben. Es ist keine Template-Engine mit eigener Syntax, sondern ein vollwertiger TypeScript/JSX-Renderer. + +Es optimiert den JSX-Code zur Laufzeit und speichert das Ergebnis im Cache. Es ist daher sehr schnell und hat nahezu keinen Overhead. + + +## JSX + +JSX ist eine Syntaxerweiterung für JavaScript und wird von TypeScript standardmäßig unterstützt. Es ermöglicht, HTML in TypeScript zu schreiben. Es ist sehr ähnlich zu Vue.js oder React.js. + +```tsx app=app.ts +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from "@deepkit/http"; + +export function View() { + return
+

Hello World

+

My first JSX view

+
; +} + +const app = new App({}); +const router = app.get(HttpRouterRegistry); + +router.get('/', () => ); + +app.run(); +``` + +```sh \ No newline at end of file diff --git a/website/src/translations/de/documentation/index.md b/website/src/translations/de/documentation/index.md new file mode 100644 index 000000000..b1a70db8c --- /dev/null +++ b/website/src/translations/de/documentation/index.md @@ -0,0 +1,88 @@ +# Dokumentation + +Deepkit ist ein Open-Source-TypeScript-Framework für Backend-Anwendungen, frei unter der MIT-Lizenz verfügbar, entwickelt, um Ihnen beim Erstellen skalierbarer und wartbarer Backend-Anwendungen zu helfen. Es ist dafür ausgelegt, im Browser und in Node.js zu funktionieren, kann jedoch in jeder geeigneten JavaScript-Umgebung laufen. + +Hier finden Sie Kapitel zu den verschiedenen Komponenten von Deepkit und API-Referenzen für alle unsere Pakete. + +Wenn Sie Hilfe benötigen, treten Sie gerne unserem [Discord-Server](https://discord.com/invite/PtfVf7B8UU) bei oder eröffnen Sie ein Issue +auf [GitHub](https://github.com/deepkit/deepkit-framework). + +## Kapitel + + +- [App](/documentation/app.md) - Schreiben Sie Ihre erste Anwendung mit Deepkit basierend auf der Befehlszeilenschnittstelle. +- [Framework](/documentation/framework.md) - Fügen Sie Ihrer Anwendung (HTTP/RPC-)Server, API-Dokumentation, Debugger, Integrationstests und mehr hinzu. +- [Runtime Types](/documentation/runtime-types.md) - Erfahren Sie mehr über TypeScript-Laufzeit-Typen sowie das Validieren und Transformieren von Daten. +- [Dependency Injection](/documentation/dependency-injection.md) - Dependency-Injection-Container, Inversion of Control und Abhängigkeitsumkehr. +- [Filesystem](/documentation/filesystem.md) - Dateisystemabstraktion zur einheitlichen Arbeit mit lokalen und entfernten Dateisystemen. +- [Broker](/documentation/broker.md) - Message-Broker-Abstraktion für verteilten L2-Cache, Pub/Sub, Queues, zentrale atomare Sperren oder Key-Value-Store. +- [HTTP](/documentation/http.md) - HTTP-Server-Abstraktion zum Aufbau typsicherer Endpunkte. +- [RPC](/documentation/rpc.md) - Abstraktion für Remote Procedure Calls, um Frontend mit Backend zu verbinden oder mehrere Backend-Dienste zu koppeln. +- [ORM](/documentation/orm.md) - ORM und DBAL, um Daten typsicher zu speichern und abzufragen. +- [Desktop-UI](/documentation/desktop-ui/getting-started) - Erstellen Sie GUI-Anwendungen mit dem auf Angular basierenden UI-Framework von Deepkit. + +## API-Referenz + +Im Folgenden finden Sie eine vollständige Liste aller Deepkit-Pakete mit Links zu deren API-Dokumentation. + +### Komposition + +- [@deepkit/app](/documentation/package/app.md) +- [@deepkit/framework](/documentation/package/framework.md) +- [@deepkit/http](/documentation/package/http.md) +- [@deepkit/angular-ssr](/documentation/package/angular-ssr.md) + +### Infrastruktur + +- [@deepkit/rpc](/documentation/package/rpc.md) +- [@deepkit/rpc-tcp](/documentation/package/rpc-tcp.md) +- [@deepkit/broker](/documentation/package/broker.md) +- [@deepkit/broker-redis](/documentation/package/broker-redis.md) + +### Dateisystem + +- [@deepkit/filesystem](/documentation/package/filesystem.md) +- [@deepkit/filesystem-ftp](/documentation/package/filesystem-ftp.md) +- [@deepkit/filesystem-sftp](/documentation/package/filesystem-sftp.md) +- [@deepkit/filesystem-s3](/documentation/package/filesystem-s3.md) +- [@deepkit/filesystem-google](/documentation/package/filesystem-google.md) +- [@deepkit/filesystem-database](/documentation/package/filesystem-database.md) + +### Datenbank + +- [@deepkit/orm](/documentation/package/orm.md) +- [@deepkit/mysql](/documentation/package/mysql.md) +- [@deepkit/postgres](/documentation/package/postgres.md) +- [@deepkit/sqlite](/documentation/package/sqlite.md) +- [@deepkit/mongodb](/documentation/package/mongodb.md) + +### Grundlagen + +- [@deepkit/type](/documentation/package/type.md) +- [@deepkit/event](/documentation/package/event.md) +- [@deepkit/injector](/documentation/package/injector.md) +- [@deepkit/template](/documentation/package/template.md) +- [@deepkit/logger](/documentation/package/logger.md) +- [@deepkit/workflow](/documentation/package/workflow.md) +- [@deepkit/stopwatch](/documentation/package/stopwatch.md) + +### Werkzeuge + +- [@deepkit/api-console](/documentation/package/api-console.md) +- [@deepkit/devtool](/documentation/package/devtool.md) +- [@deepkit/desktop-ui](/documentation/package/desktop-ui.md) +- [@deepkit/orm-browser](/documentation/package/orm-browser.md) +- [@deepkit/bench](/documentation/package/bench.md) +- [@deepkit/run](/documentation/package/run.md) + +### Kern + +- [@deepkit/bson](/documentation/package/bson.md) +- [@deepkit/core](/documentation/package/core.md) +- [@deepkit/topsort](/documentation/package/topsort.md) + +### Laufzeit + +- [@deepkit/vite](/documentation/package/vite.md) +- [@deepkit/bun](/documentation/package/bun.md) +- [@deepkit/type-compiler](/documentation/package/type-compiler.md) \ No newline at end of file diff --git a/website/src/translations/de/documentation/introduction.md b/website/src/translations/de/documentation/introduction.md new file mode 100644 index 000000000..c3453cf2c --- /dev/null +++ b/website/src/translations/de/documentation/introduction.md @@ -0,0 +1,45 @@ +# Einführung + +TypeScript hat sich als hoch skalierbare Obermenge von JavaScript etabliert, die für die Entwicklung sichererer und robusterer Anwendungen entwickelt wurde. Während JavaScript eine beträchtliche Entwicklergemeinschaft und ein großes Ökosystem aufgebaut hat, bringt TypeScript die Stärke statischer Typisierung in JavaScript ein, reduziert Laufzeitfehler erheblich und macht Codebasen leichter wartbar und verständlich. Trotz seiner Vorteile wurde das Potenzial von TypeScript jedoch nicht vollständig ausgeschöpft, insbesondere bei der Umsetzung komplexer Enterprise‑Lösungen. TypeScript verwirft inhärent seine Typinformationen während der Kompilierung, wodurch eine entscheidende Lücke in der Laufzeitfunktionalität entsteht und allerlei unergonomische Workarounds nötig werden, um Typinformationen beizubehalten. Ob Codegenerierung, einschränkende Dekoratoren oder benutzerdefinierte Typ‑Builder mit einem komplexen Inferenzschritt wie Zod – all diese Lösungen sind umständlich, langsam und fehleranfällig. Das führt nicht nur zu geringerer Entwicklungsgeschwindigkeit, sondern auch zu weniger robusten Anwendungen, insbesondere in großen Teams und komplexen Projekten. + +Hier kommt Deepkit ins Spiel, ein Framework, das revolutioniert, wie TypeScript zum Aufbau komplexer und effizienter Softwarelösungen eingesetzt werden kann. In TypeScript und für TypeScript entwickelt, ermöglicht Deepkit nicht nur Typsicherheit während der Entwicklung, sondern erweitert die Vorteile des Typsystems von TypeScript auf die Laufzeit. Indem Typinformationen zur Laufzeit beibehalten werden, öffnet Deepkit die Tür zu einer Vielzahl neuer Funktionalitäten, darunter dynamische Typberechnung, Datenvalidierung und Serialisierung, deren Implementierung zuvor umständlich war. + +Obwohl Deepkit darauf ausgelegt ist, Projekten mit hoher Komplexität und Enterprise‑Anwendungen gerecht zu werden, machen seine Agilität und modulare Architektur es gleichermaßen für kleinere Anwendungen geeignet. Die umfangreichen Bibliotheken decken gängige Anwendungsfälle ab und können je nach Projektbedarf entweder einzeln oder gemeinsam genutzt werden. Deepkit zielt darauf ab, so flexibel wie nötig und so strukturiert wie erforderlich zu sein, sodass Entwickler kurz‑ wie langfristig hohe Entwicklungsgeschwindigkeit beibehalten können. + +## Warum Deepkit? + +Das TypeScript‑Ökosystem ist reich an Bibliotheken und Tools und bietet Lösungen für nahezu jedes erdenkliche Problem. Diese Fülle an Auswahlmöglichkeiten ist zwar bereichernd, führt jedoch oft zu Komplexität aufgrund uneinheitlicher Philosophien, APIs und Codequalitäten zwischen verschiedenen Bibliotheken. Die Integration dieser disparaten Komponenten erfordert zusätzliche Abstraktionen und resultiert oft in viel Glue‑Code, der schnell außer Kontrolle gerät und die Entwicklungsgeschwindigkeit drastisch verringert. Deepkit zielt darauf ab, diese Herausforderungen zu entschärfen, indem es ein einheitliches Framework bereitstellt, das Kernfunktionen zusammenführt, die praktisch jedes Projekt benötigt. Das harmonisierte Set an Bibliotheken und Komponenten ist dafür ausgelegt, nahtlos zusammenzuarbeiten, und überbrückt damit die Lücken im fragmentierten TypeScript‑Ökosystem. + +### Bewährte Enterprise‑Prinzipien + +Deepkit lässt sich von etablierten Enterprise‑Frameworks wie Spring (Java) sowie Laravel und Symfony (PHP) inspirieren. Diese Frameworks haben sich über Jahrzehnte hinweg in Effizienz und Robustheit bewährt und bilden das Rückgrat unzähliger erfolgreicher Projekte. Deepkit bringt ähnliche Enterprise‑Designmuster und Konzepte auf neue und einzigartige Weise in die TypeScript‑Welt, sodass Entwickler von jahrelanger kollektiver Erfahrung profitieren. + +Diese Muster bieten nicht nur eine bewährte Art, Anwendungen zu strukturieren, sondern erleichtern auch die Entwicklung, insbesondere in großen Teams. Durch die Nutzung dieser bewährten Methodiken zielt Deepkit darauf ab, ein Maß an Zuverlässigkeit und Skalierbarkeit zu bieten, das in der TypeScript‑Landschaft bisher schwer zu erreichen war. + +### Agilität / Langfristige Performance + +Deepkit wurde mit Blick auf Agilität entwickelt und bietet Tools und Features, die die anfängliche Entwicklung beschleunigen und langfristig Vorteile bei der Wartung bieten. Anders als manche Frameworks, die anfängliche Geschwindigkeit auf Kosten zukünftiger Skalierbarkeit priorisieren, findet Deepkit eine Balance zwischen beidem. Seine Designmuster sind leicht zu erfassen und machen den Einstieg einfach. Gleichzeitig skalieren sie effektiv, sodass die Entwicklungsgeschwindigkeit auch bei wachsendem Projekt und Team nicht nachlässt. + +Dieser vorausschauende Ansatz macht Deepkit nicht nur zur idealen Wahl für schnelle MVPs, sondern auch für komplexe, langlebige Enterprise‑Anwendungen. + +### Developer Experience + +Schließlich legt Deepkit großen Wert auf die Developer Experience. Das Framework bietet intuitive APIs, ausführliche Dokumentation und eine unterstützende Community – alles darauf ausgerichtet, Entwicklern zu helfen, sich auf die Lösung von Geschäftsproblemen zu konzentrieren, statt mit technischen Komplexitäten zu ringen. Ob Sie eine kleine Anwendung oder ein großes System auf Enterprise‑Niveau entwickeln – Deepkit stellt die Tools und Praktiken bereit, die Ihren Entwicklungsweg reibungslos und lohnend machen. + +## Hauptfunktionen + +### Laufzeit‑Typen + +Eine der herausragenden Funktionen von Deepkit ist die Fähigkeit, Typinformationen zur Laufzeit beizubehalten. Traditionelle TypeScript‑Frameworks verwerfen diese entscheidenden Daten häufig während des Kompilierungsprozesses, was Laufzeitoperationen wie Datenvalidierung, Serialisierung oder Dependency Injection deutlich umständlicher macht. Deepkits Typ‑Compiler ermöglicht es einzigartig, Typen zur Laufzeit dynamisch zu berechnen und bestehende Typinformationen auszulesen. Das bietet nicht nur größere Flexibilität, sondern führt auch zu robusteren und typsicheren Anwendungen und vereinfacht die Entwicklung komplexer Systeme. + +### Umfassende Bibliothekssuite + +Deepkit stellt ein vollständiges Ökosystem aus Bibliotheken bereit, das die Entwicklung in verschiedensten Bereichen beschleunigt. Von Datenbankabstraktion und CLI‑Parsern über HTTP‑Router bis hin zu RPC‑Frameworks bietet Deepkit eine einheitliche Lösung für unterschiedliche Programmieranforderungen. Alle diese Bibliotheken profitieren zusätzlich davon, das Typsystem von TypeScript zur Laufzeit zu nutzen, was Boilerplate erheblich reduziert und die Code‑Klarheit erhöht. Die Modularität von Deepkit erlaubt es Entwicklern, einzelne Bibliotheken für spezifische Aufgaben zu nutzen oder das gesamte Framework einzusetzen, um vollständige, produktionsreife Anwendungen zu erstellen. + +## Hohe Performance & Skalierbarkeit + +Die Entwicklungsgeschwindigkeit bei zunehmender Projektkomplexität aufrechtzuerhalten, ist eine gewaltige Herausforderung. Deepkit geht dieses Problem direkt an, indem es den Einsatz bewährter Enterprise‑Designmuster betont, die mit größeren Teams und komplexeren Codebasen gut skalieren. Das Framework integriert etablierte Enterprise‑Designmuster, die sich als gut skalierend mit größeren Teams und komplexeren Codebasen erwiesen haben. Deepkits Ansatz stellt sicher, dass Projekte nicht nur in den Anfangsphasen, sondern während ihres gesamten Lebenszyklus agil und effizient bleiben. Dies wird erreicht, indem Boilerplate minimiert und Designmuster so ergonomisch wie möglich eingesetzt werden, sodass Teams langfristig eine hohe Produktivität aufrechterhalten können. + +### Isomorphes TypeScript + +Deepkit ist darauf ausgelegt, die Vorteile von isomorphem TypeScript zu maximieren, bei dem dieselbe Codebasis auf mehreren Plattformen verwendet werden kann – sei es Frontend, Backend oder sogar mobile Anwendungen. Das führt zu erheblichen Zeit‑ und Kosteneinsparungen, da Code zwischen verschiedenen Abteilungen geteilt werden kann, was die Rekrutierung vereinfacht und die Wissensweitergabe innerhalb von Teams erleichtert. Deepkit schöpft die Leistungsfähigkeit isomorphen TypeScripts voll aus und bietet ein integriertes, plattformübergreifendes Entwicklungserlebnis, das traditionelle Dual‑Stack‑Ansätze deutlich übertrifft. \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm.md b/website/src/translations/de/documentation/orm.md new file mode 100644 index 000000000..5f3229f13 --- /dev/null +++ b/website/src/translations/de/documentation/orm.md @@ -0,0 +1,15 @@ +# Deepkit ORM + +Deepkit ORM ist ein leistungsstarkes TypeScript ORM (Object-Relational Mapper). Es bietet eine einfache und intuitive API für die Interaktion mit Datenbanken, sodass Sie sich auf den Aufbau Ihrer Anwendung konzentrieren können, anstatt sich um Low-Level-Datenbankoperationen zu kümmern. Das ORM basiert auf Deepkits Runtime Type System, das eine typsichere Umgebung für die Arbeit mit Datenbanken bereitstellt. + +## Warum ein ORM? + +Object-Relational Mapping (ORM) in Deepkit bietet Entwicklern mehrere Vorteile. + +1. Vereinfachte Datenbankoperationen: Mit einem ORM können Entwickler die manuelle Erstellung und Ausführung von SQL-Abfragen abstrahieren. Stattdessen können sie mit einem intuitiveren objektorientierten Ansatz mit der Datenbank interagieren. Das vereinfacht gängige Datenbankoperationen wie das Abfragen, Einfügen, Aktualisieren und Löschen von Datensätzen. + +2. Datenbankübergreifende Kompatibilität: Ein ORM ermöglicht es Entwicklern, datenbankunabhängigen Code zu schreiben, indem es eine konsistente API zur Interaktion mit unterschiedlichen Datenbanksystemen bereitstellt. Dadurch können Sie problemlos zwischen verschiedenen Datenbank-Engines wie MySQL, PostgreSQL oder SQLite wechseln, ohne wesentliche Änderungen an Ihrem Code vorzunehmen. + +3. Typsicherheit und Compile-Time-Checks: Durch die Nutzung von Typinformationen zur Laufzeit bietet Deepkits ORM eine typsichere Umgebung für die Arbeit mit Datenbanken. Mit dem ORM können Sie Datenbank-Schemata als TypeScript Classes oder Interfaces definieren, sodass potenzielle Errors bereits zur Compile-Time statt zur Laufzeit abgefangen werden. Zusätzlich übernimmt das ORM automatische Typumwandlungen und Validierung, wodurch sichergestellt wird, dass Ihre Daten stets konsistent sind und korrekt in der Datenbank persistiert werden. + +Insgesamt vereinfacht die Verwendung eines ORM in Deepkit die Datenbankoperationen, verbessert die datenbankübergreifende Kompatibilität und bietet Typsicherheit sowie Compile-Time-Checks, wodurch es zu einer wesentlichen Komponente für den Aufbau robuster und wartbarer Anwendungen wird. \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/composite-primary-key.md b/website/src/translations/de/documentation/orm/composite-primary-key.md new file mode 100644 index 000000000..d79411272 --- /dev/null +++ b/website/src/translations/de/documentation/orm/composite-primary-key.md @@ -0,0 +1,84 @@ +# Zusammengesetzter Primärschlüssel + +Ein zusammengesetzter Primärschlüssel bedeutet, dass eine Entity mehrere Primärschlüssel hat, die automatisch zu einem "zusammengesetzten Primärschlüssel" kombiniert werden. Diese Art, eine Datenbank zu modellieren, hat Vor- und Nachteile. Wir sind der Meinung, dass zusammengesetzte Primärschlüssel erhebliche praktische Nachteile mit sich bringen, die ihre Vorteile nicht rechtfertigen; sie sollten daher als Bad Practice betrachtet und vermieden werden. Deepkit ORM unterstützt keine zusammengesetzten Primärschlüssel. In diesem Kapitel erklären wir warum und zeigen (bessere) Alternativen. + +## Nachteile + +Joins sind nicht trivial. Obwohl sie in RDBMS stark optimiert sind, bedeuten sie in Anwendungen eine konstante Komplexität, die leicht aus dem Ruder laufen und zu Performance-Problemen führen kann. Performance nicht nur im Hinblick auf die Ausführungszeit von Abfragen, sondern auch in Bezug auf Entwicklungszeit. + +## Joins + +Jeder einzelne Join wird komplizierter, je mehr Felder beteiligt sind. Zwar haben viele Datenbanken Optimierungen implementiert, damit Joins mit mehreren Feldern nicht per se langsamer sind; dennoch muss der Entwickler diese Joins ständig im Detail durchdenken, da das Vergessen von Schlüsseln beispielsweise zu subtilen Fehlern führen kann (der Join funktioniert nämlich auch ohne Angabe aller Schlüssel), und der Entwickler daher die vollständige Struktur des zusammengesetzten Primärschlüssels kennen muss. + +## Indizes + +Indizes mit mehreren Feldern (also zusammengesetzte Primärschlüssel) leiden unter dem Problem der Feldreihenfolge in Abfragen. Zwar können Datenbanksysteme bestimmte Abfragen optimieren, doch erschweren komplexe Strukturen das Schreiben effizienter Operationen, die alle definierten Indizes korrekt nutzen. Für einen Index mit mehreren Feldern (wie einen zusammengesetzten Primärschlüssel) ist es in der Regel notwendig, die Felder in der richtigen Reihenfolge anzugeben, damit die Datenbank den Index tatsächlich verwendet. Wird die Reihenfolge nicht korrekt angegeben (z. B. in einer WHERE-Klausel), kann das leicht dazu führen, dass die Datenbank den Index gar nicht nutzt und stattdessen einen Full Table Scan ausführt. Zu wissen, welche Datenbank welche Abfrage wie optimiert, ist fortgeschrittenes Wissen, das neue Entwickler üblicherweise nicht haben, das aber erforderlich wird, sobald man mit zusammengesetzten Primärschlüsseln arbeitet, um das Maximum aus der Datenbank herauszuholen und keine Ressourcen zu verschwenden. + +## Migrationen + +Sobald Sie entscheiden, dass eine bestimmte Entity ein zusätzliches Feld benötigt, um sie eindeutig zu identifizieren (und damit Teil des zusammengesetzten Primärschlüssels wird), müssen alle Entities in Ihrer Datenbank angepasst werden, die Beziehungen zu dieser Entity haben. + +Angenommen, Sie haben eine Entity `user` mit zusammengesetztem Primärschlüssel und entscheiden sich, in verschiedenen Tabellen, z. B. in einer Pivot-Tabelle `audit_log`, `groups` und `posts`, einen Foreign Key auf diesen `user` zu verwenden. Sobald Sie den Primärschlüssel von `user` ändern, müssen all diese Tabellen in einer Migration ebenfalls angepasst werden. + +Das macht nicht nur Migrationsdateien deutlich komplexer, sondern kann beim Ausführen der Migrationen auch zu erheblicher Downtime führen, da Schemaänderungen üblicherweise entweder einen vollständigen Datenbank-Lock oder zumindest einen Table Lock erfordern. Je mehr Tabellen von einer großen Änderung wie einer Index-Änderung betroffen sind, desto länger dauert die Migration. Und je größer eine Tabelle ist, desto länger dauert die Migration. +Betrachten Sie die Tabelle `audit_log`. Solche Tabellen haben in der Regel viele Datensätze (Millionen o. ä.), und Sie müssen sie im Rahmen einer Schemaänderung anfassen – nur weil Sie sich entschieden haben, einen zusammengesetzten Primärschlüssel zu verwenden und dem Primärschlüssel von `user` ein zusätzliches Feld hinzuzufügen. Abhängig von der Größe all dieser Tabellen macht das Migrationsänderungen entweder unnötig teurer oder in manchen Fällen so teuer, dass eine Änderung des Primärschlüssels von `user` finanziell nicht mehr zu rechtfertigen ist. Das führt üblicherweise zu Workarounds (z. B. dem Hinzufügen eines Unique Index zur user-Tabelle), die technische Schulden erzeugen und früher oder später auf der Legacy-Liste landen. + +Bei großen Projekten kann das zu enormer Downtime (von Minuten bis Stunden) führen und manchmal sogar zur Einführung eines völlig neuen Migrations-Abstraktionssystems, das im Wesentlichen Tabellen kopiert, Datensätze in Ghost Tables einfügt und Tabellen nach der Migration hin- und herschiebt. Diese zusätzliche Komplexität wird wiederum jeder Entity auferlegt, die eine Beziehung zu einer anderen Entity mit zusammengesetztem Primärschlüssel hat, und sie wird umso größer, je größer Ihre Datenbankstruktur wird. Das Problem verschärft sich, ohne dass es eine Möglichkeit gibt, es zu lösen (außer den zusammengesetzten Primärschlüssel vollständig zu entfernen). + +## Auffindbarkeit + +Wenn Sie Datenbankadministrator oder Data Engineer/Scientist sind, arbeiten Sie üblicherweise direkt auf der Datenbank und explorieren die Daten bei Bedarf. Bei zusammengesetzten Primärschlüsseln muss jeder Nutzer, der direkt SQL schreibt, den korrekten Primärschlüssel aller beteiligten Tabellen kennen (samt Spaltenreihenfolge, um korrekte Index-Optimierungen zu erhalten). Dieser zusätzliche Overhead erschwert nicht nur die Datenexploration, Berichtserstellung usw., sondern kann auch zu Fehlern in älterem SQL führen, wenn ein zusammengesetzter Primärschlüssel plötzlich geändert wird. Das alte SQL ist vermutlich weiterhin gültig und läuft problemlos, liefert aber plötzlich falsche Ergebnisse, weil im Join das neue Feld des zusammengesetzten Primärschlüssels fehlt. Es ist deutlich einfacher, nur einen Primärschlüssel zu haben. Das erleichtert das Auffinden von Daten und stellt sicher, dass alte SQL-Abfragen weiterhin korrekt funktionieren, wenn Sie sich z. B. entscheiden, die eindeutige Identifikation eines User-Objekts zu ändern. + +## Refactoring + +Sobald in einer Entity ein zusammengesetzter Primärschlüssel verwendet wird, kann das Refactoring des Schlüssels zu erheblich mehr zusätzlichem Refactoring führen. Da eine Entity mit zusammengesetztem Primärschlüssel typischerweise kein einzelnes eindeutiges Feld hat, müssen alle Filter und Verknüpfungen alle Werte des zusammengesetzten Schlüssels enthalten. Das bedeutet meist, dass sich der Code darauf verlässt, den zusammengesetzten Primärschlüssel zu kennen, sodass alle Felder verfügbar sein müssen (z. B. für URLs wie user:key1:key2). Sobald dieser Schlüssel geändert wird, müssen alle Stellen, an denen dieses Wissen explizit verwendet wird – etwa URLs, individuelles SQL und andere Orte – neu geschrieben werden. + +Während ORMs Joins typischerweise automatisch erstellen, ohne die Werte manuell anzugeben, können sie Refactorings für alle anderen Anwendungsfälle wie URL-Strukturen oder individuelles SQL nicht automatisch abdecken – und schon gar nicht für Orte, an denen das ORM überhaupt nicht verwendet wird, etwa in Reporting-Systemen und allen externen Systemen. + +## ORM-Komplexität + +Mit der Unterstützung zusammengesetzter Primärschlüssel steigt die Komplexität des Codes eines leistungsfähigen ORMs wie Deepkit ORM enorm. Nicht nur Code und Wartung werden komplexer und damit teurer, es gibt auch mehr Edge Cases von Nutzern, die behoben und gepflegt werden müssen. Die Komplexität der Query-Schicht, der Change-Detection, des Migrationssystems, des internen Relationship-Trackings usw. nimmt erheblich zu. Die Gesamtkosten, die mit dem Aufbau und der Unterstützung eines ORMs mit zusammengesetzten Primärschlüsseln verbunden sind, sind unter Berücksichtigung aller Faktoren zu hoch und nicht zu rechtfertigen – weshalb Deepkit sie nicht unterstützt. + +## Vorteile + +Abgesehen davon haben zusammengesetzte Primärschlüssel auch Vorteile, wenn auch sehr oberflächliche. Durch die Verwendung möglichst weniger Indizes pro Tabelle wird das Schreiben (Insert/Update) von Daten effizienter, da weniger Indizes gepflegt werden müssen. Zudem wird die Struktur des Modells etwas schlanker (da es üblicherweise eine Spalte weniger hat). Der Unterschied zwischen einem sequentiell angeordneten, automatisch inkrementierenden Primärschlüssel und einem nicht-inkrementierenden Primärschlüssel ist heutzutage jedoch völlig vernachlässigbar, da Speicherplatz günstig ist und der Vorgang in der Regel eine "append-only"-Operation ist, die sehr schnell ist. + +Es kann sicherlich einige Edge Cases geben (und für ein paar sehr spezifische Datenbanksysteme), in denen es anfänglich besser erscheint, mit zusammengesetzten Primärschlüsseln zu arbeiten. Aber selbst in diesen Systemen könnte es insgesamt sinnvoller sein (unter Berücksichtigung aller Kosten), sie nicht zu verwenden und auf eine andere Strategie umzusteigen. + +## Alternative + +Eine Alternative zu zusammengesetzten Primärschlüsseln besteht darin, einen einzelnen automatisch inkrementierenden numerischen Primärschlüssel zu verwenden, üblicherweise "id" genannt, und den zusammengesetzten Primärschlüssel in einen eindeutigen Index mit mehreren Feldern zu verlagern. Je nach verwendetem Primärschlüssel (abhängig von der erwarteten Anzahl von Zeilen) verwendet die "id" entweder 4 oder 8 Byte pro Datensatz. + +Mit dieser Strategie sind Sie nicht mehr gezwungen, über die oben beschriebenen Probleme nachzudenken und eine Lösung zu finden, was die Kosten für immer größer werdende Projekte enorm reduziert. + +Die Strategie bedeutet konkret, dass jede Entity ein "id"-Feld hat, üblicherweise ganz am Anfang, und dieses Feld wird dann standardmäßig zur eindeutigen Identifikation von Zeilen und in Joins verwendet. + +```typescript +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor(public username: string) {} +} +``` + +Als Alternative zu einem zusammengesetzten Primärschlüssel würden Sie stattdessen einen eindeutigen Index über mehrere Felder verwenden. + +```typescript +@entity.index(['tenancyId', 'username'], {unique: true}) +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public tenancyId: number, + public username: string, + ) {} +} +``` + +Deepkit ORM unterstützt automatisch inkrementierende Primärschlüssel, auch für MongoDB. Dies ist die bevorzugte Methode zur Identifikation von Datensätzen in Ihrer Datenbank. Für MongoDB können Sie jedoch auch die ObjectId (`_id: MongoId & PrimaryKey = ''`) als einfachen Primärschlüssel verwenden. Eine Alternative zum numerischen, automatisch inkrementierenden Primärschlüssel ist eine UUID, die genauso gut funktioniert (hat aber leicht andere Performance-Eigenschaften, da die Indexierung teurer ist). + +## Zusammenfassung + +Zusammengesetzte Primärschlüssel bedeuten im Wesentlichen, dass alle zukünftigen Änderungen und die praktische Nutzung ab dem Zeitpunkt ihrer Einführung deutlich höhere Kosten verursachen. Während es anfangs wie eine saubere Architektur wirkt (weil man eine Spalte weniger hat), führt es zu erheblichen praktischen Kosten, sobald das Projekt tatsächlich entwickelt wird, und die Kosten steigen weiter, je größer das Projekt wird. + +Betrachtet man die Asymmetrien zwischen Nutzen und Nachteilen, wird klar, dass zusammengesetzte Primärschlüssel in den meisten Fällen nicht zu rechtfertigen sind. Die Kosten sind deutlich größer als der Nutzen – nicht nur für Sie als Nutzer, sondern auch für uns als Autor und Maintainer des ORM-Codes. Aus diesem Grund unterstützt Deepkit ORM keine zusammengesetzten Primärschlüssel. \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/entity.md b/website/src/translations/de/documentation/orm/entity.md new file mode 100644 index 000000000..e56f8400b --- /dev/null +++ b/website/src/translations/de/documentation/orm/entity.md @@ -0,0 +1,226 @@ +# Entity + +Eine Entity ist entweder eine Class oder ein Object Literal (Interface) und hat immer einen Primary Key. +Die Entity wird mit allen notwendigen Informationen über Type Annotations aus `@deepkit/type` versehen. Zum Beispiel wird ein Primary Key definiert sowie verschiedene Felder und deren Validierungs-Constraints. Diese Felder spiegeln die Datenbankstruktur wider, in der Regel eine Tabelle oder eine Collection. + +Durch spezielle Type Annotations wie `Mapped<'name'>` kann ein Feldname auch auf einen anderen Namen in der Datenbank gemappt werden. + +## Class + +```typescript +import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: string & Unique & MinLength<2> & MaxLength<16>, + public email: string & Unique, + ) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +await database.migrate(); + +await database.persist(new User('Peter')); + +const allUsers = await database.query(User).find(); +console.log('all users', allUsers); +``` + +## Interface + +```typescript +import { PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + username: string & Unique & MinLength<2> & MaxLength<16>; +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:')); +database.register({name: 'user'}); + +await database.migrate(); + +const user: User = {id: 0, created: new Date, username: 'Peter'}; +await database.persist(user); + +const allUsers = await database.query().find(); +console.log('all users', allUsers); +``` + +## Primitive + +Primitive Datentypen wie String, Number (bigint) und Boolean werden auf gängige Datenbanktypen gemappt. Es wird nur der TypeScript Type verwendet. + +```typescript + +interface User { + logins: number; + username: string; + pro: boolean; +} +``` + +## Primary Key + +Jede Entity benötigt genau einen Primary Key. Mehrere Primary Keys werden nicht unterstützt. + +Der Basistype eines Primary Keys kann beliebig sein. Häufig wird eine number oder eine UUID verwendet. +Für MongoDB wird oft die MongoId oder ObjectID genutzt. + +Für Zahlen kann `AutoIncrement` verwendet werden. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## Auto Increment + +Felder, die beim Insert automatisch inkrementiert werden sollen, werden mit dem `AutoIncrement` Decorator annotiert. Alle Adapter unterstützen Auto-Increment-Werte. Der MongoDB Adapter verwendet eine zusätzliche Collection, um den Zähler nachzuverfolgen. + +Ein Auto-Increment-Feld ist ein automatischer Zähler und kann nur auf einen Primary Key angewendet werden. Die Datenbank stellt automatisch sicher, dass eine ID nur einmal verwendet wird. + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## UUID + +Felder, die vom Type UUID (v4) sein sollen, werden mit dem Decorator UUID annotiert. Der Runtime Type ist `string` und in der Datenbank selbst meist binär. Verwende die Function `uuid()`, um eine neue UUID v4 zu erzeugen. + +```typescript +import { uuid, UUID, PrimaryKey } from '@deepkit/type'; + +class User { + id: UUID & PrimaryKey = uuid(); +} +``` + +## MongoDB ObjectID + +Felder, die in MongoDB vom Type ObjectID sein sollen, werden mit dem Decorator `MongoId` annotiert. Der Runtime Type ist `string` und in der Datenbank selbst `ObjectId` (binär). + +MongoID-Felder erhalten beim Insert automatisch einen neuen Wert. Es ist nicht zwingend erforderlich, den Feldnamen `_id` zu verwenden. Er kann beliebig sein. + +```typescript +import { PrimaryKey, MongoId } from '@deepkit/type'; + +class User { + id: MongoId & PrimaryKey = ''; +} +``` + +## Optional / Nullable + +Optionale Felder werden als TypeScript Type mit `title?: string` oder `title: string | null` deklariert. Du solltest nur eine dieser Varianten verwenden, üblicherweise die optionale `?`-Syntax, die mit `undefined` funktioniert. +Beide Varianten führen dazu, dass der Datenbanktyp bei allen SQL Adaptern `NULLABLE` ist. Der einzige Unterschied zwischen diesen Varianten ist, dass sie zur Runtime unterschiedliche Werte repräsentieren. + +Im folgenden Beispiel ist das Feld modified optional und kann zur Runtime daher undefined sein, obwohl es in der Datenbank immer als NULL dargestellt wird. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified?: Date; +} +``` + +Dieses Beispiel zeigt, wie der Nullable Type funktioniert. NULL wird sowohl in der Datenbank als auch in der JavaScript-Runtime verwendet. Das ist ausführlicher als `modified?: Date` und wird nicht häufig genutzt. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified: Date | null = null; +} +``` + +## Datenbank-Typ-Mapping + +|=== +|Laufzeittyp|SQLite|MySQL|Postgres|Mongo + +|string|text|longtext|text|string +|number|float|double|double precision|int/number +|boolean|integer(1)|boolean|boolean|boolean +|date|text|datetime|timestamp|datetime +|array|text|json|jsonb|array +|map|text|json|jsonb|object +|map|text|json|jsonb|object +|union|text|json|jsonb|T +|uuid|blob|binary(16)|uuid|binary +|ArrayBuffer/Uint8Array/...|blob|longblob|bytea|binary +|=== + +Mit `DatabaseField` ist es möglich, ein Feld auf jeden beliebigen Datenbanktyp zu mappen. Der Type muss ein gültiges SQL-Statement sein, das unverändert an das Migrationssystem weitergegeben wird. + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + title: string & DatabaseField<{type: 'VARCHAR(244)'}>; +} +``` + +Um ein Feld für eine spezifische Datenbank zu mappen, können entweder `SQLite`, `MySQL` oder `Postgres` verwendet werden. + +### SQLite + +```typescript +import { SQLite } from '@deepkit/type'; + +interface User { + title: string & SQLite<{type: 'text'}>; +} +``` + +### MySQL + +```typescript +import { MySQL } from '@deepkit/type'; + +interface User { + title: string & MySQL<{type: 'text'}>; +} +``` + +### Postgres + +```typescript +import { Postgres } from '@deepkit/type'; + +interface User { + title: string & Postgres<{type: 'text'}>; +} +``` + +## Eingebettete Types + +## Standardwerte + +## Default-Ausdrücke + +## Komplexe Types + +## Exclude + +## Datenbankspezifische Spaltentypen \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/events.md b/website/src/translations/de/documentation/orm/events.md new file mode 100644 index 000000000..2f489b4d0 --- /dev/null +++ b/website/src/translations/de/documentation/orm/events.md @@ -0,0 +1,65 @@ +# Events + +Events sind eine Möglichkeit, in Deepkit ORM einzuhaken und ermöglichen es Ihnen, leistungsfähige Plugins zu schreiben. Es gibt zwei +Kategorien von Events: Query-Events und Unit-of-Work-Events. Plugin-Autoren verwenden typischerweise beide, um beide Wege der Datenmanipulation zu unterstützen. + +Events werden über `Database.listen` mit einem Event-Token registriert. Kurzlebige Event-Listener können auch +an Sessions registriert werden. + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); +database.listen(Query.onFetch, async (event) => { +}); + +const session = database.createSession(); + +//wird nur für diese bestimmte Session ausgeführt +session.eventDispatcher.listen(Query.onFetch, async (event) => { +}); +``` + +## Query-Events + +Query-Events werden ausgelöst, wenn eine Query über `Database.query()` oder `Session.query()` ausgeführt wird. + +Jedes Event hat eigene zusätzliche Properties, wie z. B. den Entity-Typ, die Query selbst und die Datenbank-Session. Sie können die Query überschreiben, indem Sie eine neue Query auf `Event.query` setzen. + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); + +const unsubscribe = database.listen(Query.onFetch, async event => { + //überschreibt die Query des Benutzers, sodass etwas anderes ausgeführt wird. + event.query = event.query.filterField('fieldName', 123); +}); + +//um den Hook zu entfernen, unsubscribe aufrufen +unsubscribe(); +``` + +"Query" hat mehrere Event-Tokens: + +| Event-Token | Beschreibung | +|--------------------|------------------------------------------------------------| +| Query.onFetch | Wenn Objekte über find()/findOne()/etc abgerufen wurden | +| Query.onDeletePre | Bevor Objekte über deleteMany/deleteOne() gelöscht werden | +| Query.onDeletePost | Nachdem Objekte über deleteMany/deleteOne() gelöscht wurden | +| Query.onPatchPre | Bevor Objekte über patchMany/patchOne() gepatcht/aktualisiert werden | +| Query.onPatchPost | Nachdem Objekte über patchMany/patchOne() gepatcht/aktualisiert wurden | + +## Unit-of-Work-Events + +Unit-of-Work-Events werden ausgelöst, wenn eine neue Session Änderungen übermittelt. + +| Event-Token | Beschreibung | +|------------------------------|----------------------------------------------------------------------------------------------------------------------| +| DatabaseSession.onUpdatePre | Ausgelöst unmittelbar bevor das Objekt `DatabaseSession` eine Update-Operation an den Datenbank-Datensätzen startet. | +| DatabaseSession.onUpdatePost | Ausgelöst unmittelbar nachdem das Objekt `DatabaseSession` die Update-Operation erfolgreich abgeschlossen hat. | +| DatabaseSession.onInsertPre | Ausgelöst unmittelbar bevor das Objekt `DatabaseSession` das Einfügen neuer Datensätze in die Datenbank startet. | +| DatabaseSession.onInsertPost | Ausgelöst unmittelbar nachdem das Objekt `DatabaseSession` die neuen Datensätze erfolgreich eingefügt hat. | +| DatabaseSession.onDeletePre | Ausgelöst unmittelbar bevor das Objekt `DatabaseSession` eine Delete-Operation zum Entfernen von Datensätzen beginnt. | +| DatabaseSession.onDeletePost | Ausgelöst unmittelbar nachdem das Objekt `DatabaseSession` die Delete-Operation abgeschlossen hat. | +| DatabaseSession.onCommitPre | Ausgelöst unmittelbar bevor das Objekt `DatabaseSession` während der Session vorgenommene Änderungen in der Datenbank committet. | \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/getting-started.md b/website/src/translations/de/documentation/orm/getting-started.md new file mode 100644 index 000000000..d8fc48a2b --- /dev/null +++ b/website/src/translations/de/documentation/orm/getting-started.md @@ -0,0 +1,191 @@ +# Erste Schritte + +Deepkit bietet eine Datenbank-ORM, die den Zugriff auf Datenbanken auf moderne Weise ermöglicht. +Entitäten werden einfach mit TypeScript Types definiert: + +```typescript +import { entity, PrimaryKey, AutoIncrement, + Unique, MinLength, MaxLength } from '@deepkit/type'; + +type Username = string & Unique & MinLength<2> & MaxLength<16>; + +// Entity als Class +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: Username, + public email: string & Unique, + ) {} +} + +// oder als Interface +interface User { + id: number & PrimaryKey & AutoIncrement; + created: Date; + firstName?: string; + lastName?: string; + username: Username; + email: string & Unique; +} +``` + +Beliebige TypeScript Types und Validation Decorators von Deepkit können verwendet werden, um die Entity vollständig zu definieren. +Das Entity Type-System ist so konzipiert, dass diese Types oder Classes auch in anderen Bereichen wie HTTP Routes, RPC Actions oder Frontend verwendet werden können. Das verhindert zum Beispiel, dass man einen User mehrfach in der gesamten Anwendung verteilt definiert hat. + +## Installation + +Da Deepkit ORM auf Runtime Types basiert, muss `@deepkit/type` bereits korrekt installiert sein. +Siehe [Installation von Runtime Types](../runtime-types/getting-started.md). + +Wenn dies erfolgreich erledigt ist, können `@deepkit/orm` selbst und ein Datenbank-Adapter installiert werden. + +Wenn Classes als Entities verwendet werden sollen, muss `experimentalDecorators` in der tsconfig.json aktiviert sein: + +```json +{ + "compilerOptions": { + "experimentalDecorators": true + } +} +``` + +Sobald die Library installiert ist, kann ein Datenbank-Adapter installiert werden und dessen API kann direkt verwendet werden. + +### SQLite + +```sh +npm install @deepkit/orm @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; + +const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +``` + +### MySQL + +```sh +npm install @deepkit/orm @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; + +const database = new Database(new MySQLDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### Postgres + +```sh +npm install @deepkit/orm @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/postgres'; + +const database = new Database(new PostgresDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### MongoDB + +```sh +npm install @deepkit/orm @deepkit/bson @deepkit/mongo +``` + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(new MongoDatabaseAdapter('mongodb://localhost/mydatabase'), [User]); +``` + +## Verwendung + +In erster Linie wird das `Database` Objekt verwendet. Sobald es instanziiert wurde, kann es in der gesamten Anwendung zum Abfragen oder Manipulieren von Daten genutzt werden. Die Verbindung zur Datenbank wird lazy initialisiert. + +Dem `Database` Objekt wird ein Adapter übergeben, der aus den Datenbank-Adapter-Libraries stammt. + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + @entity.name('user') + class User { + public id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); + await database.migrate(); // Tabellen erstellen + + await database.persist(new User('Peter')); + + const allUsers = await database.query(User).find(); + console.log('all users', allUsers); +} + +main(); +``` + +### Datenbank + +### Verbindung + +#### Read Replica + +## Repository + +## Index + +## Groß-/Kleinschreibung + +## Zeichensätze + +## Collations + +## Batching + +## Caching + +## Multitenancy + +## Naming Strategy + +## Locking + +### Optimistic Locking + +### Pessimistic Locking + +## Custom Types + +## Logging + +## Migration + +## Seeding + +## Direkter Datenbankzugriff + +### SQL + +### MongoDB + +## Plugins \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/inheritance.md b/website/src/translations/de/documentation/orm/inheritance.md new file mode 100644 index 000000000..171710e0b --- /dev/null +++ b/website/src/translations/de/documentation/orm/inheritance.md @@ -0,0 +1,104 @@ +# Vererbung + +Es gibt mehrere Möglichkeiten, Vererbung in Deepkit ORM zu implementieren. + +## Klassenvererbung + +Eine Möglichkeit ist die Verwendung von Klassenvererbung, die einfache Klassen mit `extends` verwendet. + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +class BaseModel { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + updated: Date = new Date; +} + +class User extends BaseModel { + name: string = ''; +} + +class Customer extends BaseModel { + name: string = ''; + address: string = ''; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [User, Customer] +); +``` + +Da `BaseModel` nicht als Entity verwendet wird, wird es nicht in der Datenbank registriert. Nur `User` und `Customer` werden als Entities registriert und einer Tabelle zugeordnet, die alle Properties von `BaseModel` enthält. + +Die SQL-Tabellen würden so aussehen: + +```sql +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL +); + +CREATE TABLE customer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL, + address TEXT NOT NULL +); +``` + +## Single Table Inheritance + +Single Table Inheritance ist eine Möglichkeit, mehrere Entities in einer Tabelle zu speichern. Anstatt separate Tabellen für jedes Model zu haben, wird eine einzelne Tabelle verwendet, und eine zusätzliche Spalte (oft type oder ähnlich genannt) wird genutzt, um den Typ jedes Datensatzes zu bestimmen. Das ist nützlich, wenn Sie viele Entities haben, die dieselben Properties teilen. + +```typescript +import { PrimaryKey, AutoIncrement, entity } from '@deepkit/type'; + +@entity.collection('persons') +abstract class Person { + id: number & PrimaryKey & AutoIncrement = 0; + firstName?: string; + lastName?: string; + abstract type: string; +} + +@entity.singleTableInheritance() +class Employee extends Person { + email?: string; + + type: 'employee' = 'employee'; +} + +@entity.singleTableInheritance() +class Freelancer extends Person { + @t budget: number = 10_000; + + type: 'freelancer' = 'freelancer'; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [Employee, Freelancer] +); +``` + +Die Klasse `Person` ist keine Entity und wird daher nicht in der Datenbank registriert. Die Klassen `Employee` und `Freelancer` sind Entities und werden auf eine einzelne Tabelle mit dem Namen `persons` abgebildet. Die Spalte `type` wird verwendet, um den Typ jedes Datensatzes zu bestimmen. + +Eine SQL-Tabelle würde so aussehen: + +```sql +CREATE TABLE persons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + firstName TEXT, + lastName TEXT, + type TEXT NOT NULL, + email TEXT, + budget INTEGER +); +``` + +Wie Sie sehen, wird budget optional gemacht (obwohl es in der Klasse `Freelance` erforderlich ist). Das dient dazu, das Einfügen von `Employee` (das keinen budget-Wert hat) in dieselbe Tabelle zu unterstützen. Das ist eine Einschränkung von Single Table Inheritance. \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/migrations.md b/website/src/translations/de/documentation/orm/migrations.md new file mode 100644 index 000000000..9745fb83e --- /dev/null +++ b/website/src/translations/de/documentation/orm/migrations.md @@ -0,0 +1,74 @@ +# Migrationen + +Migrationen sind eine Möglichkeit, Änderungen am Datenbankschema strukturiert und organisiert vorzunehmen. Sie werden als TypeScript-Dateien in einem Verzeichnis gespeichert und können über das Kommandozeilen-Tool ausgeführt werden. + +Deepkit ORM-Migrationen sind standardmäßig aktiviert, wenn das Deepkit Framework verwendet wird. + +## Befehle + +- `migration:create` - Generiert eine neue Migrationsdatei basierend auf einem Datenbank-Diff +- `migration:pending` - Zeigt ausstehende Migrationsdateien +- `migration:up` - Führt ausstehende Migrationsdateien aus. +- `migration:down` - Führt die Down-Migration aus und macht ältere Migrationsdateien rückgängig + +Diese Befehle sind entweder in der Anwendung verfügbar, wenn Sie das `FrameworkModule` importieren, oder über das Kommandozeilen-Tool `deepkit-sql` aus `@deepkit/sql`. + +Die [Migrations-Integration des FrameworkModule](../framework/database.md#migration) liest Ihre Datenbanken automatisch (Sie müssen sie als Provider definieren), während Sie bei `deepkit-sql` die TypeScript-Datei angeben müssen, die die Datenbank exportiert. Letzteres ist nützlich, wenn Sie Deepkit ORM eigenständig ohne Deepkit Framework verwenden. + +## Eine Migration erstellen + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + name = 'default'; + constructor() { + super(new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), [User]); + } +} +``` + +```sh +./node_modules/.bin/deepkit-sql migration:create --path database.ts --migrationDir src/migrations +``` + +In `src/migrations` wird eine neue Migrationsdatei erstellt. + +Die neu erstellte Migrationsdatei enthält nun die Methoden up und down, basierend auf der Differenz zwischen den in Ihrer TypeScript-App definierten Entities und der konfigurierten Datenbank. +Sie können die Methode up nun nach Bedarf anpassen. Die Methode down wird automatisch auf Basis der Methode up generiert. +Committen Sie diese Datei in Ihr Repository, damit andere Entwickler sie ebenfalls ausführen können. + +## Ausstehende Migrationen + +```sh +./node_modules/.bin/deepkit-sql migration:pending --path database.ts --migrationDir src/migrations +``` + +Dies zeigt alle ausstehenden Migrationen. Wenn Sie eine neue Migrationsdatei haben, die noch nicht ausgeführt wurde, wird sie hier aufgelistet. + +## Migrationen ausführen + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations +``` + +Damit wird die nächste ausstehende Migration ausgeführt. + +## Migrationen rückgängig machen + +```sh +./node_modules/.bin/deepkit-sql migration:down --path database.ts --migrationDir src/migrations +``` + +Damit wird die zuletzt ausgeführte Migration rückgängig gemacht. + +## Fake-Migrationen + +Angenommen, Sie wollten eine Migration (up oder down) ausführen, aber sie ist fehlgeschlagen. Sie haben das Problem manuell behoben, können die Migration nun jedoch nicht erneut ausführen, da sie bereits ausgeführt ist. Sie können die Option `--fake` verwenden, um die Migration zu faken, sodass sie in der Datenbank als ausgeführt markiert wird, ohne sie tatsächlich auszuführen. So können Sie die nächste ausstehende Migration ausführen. + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/orm-browser.md b/website/src/translations/de/documentation/orm/orm-browser.md new file mode 100644 index 000000000..2a0e035f5 --- /dev/null +++ b/website/src/translations/de/documentation/orm/orm-browser.md @@ -0,0 +1,60 @@ +# ORM Browser + +Deepkit ORM Browser ist ein webbasiertes Tool, um das Datenbankschema und die Daten zu erkunden. Es basiert auf dem Deepkit Framework und kann mit jeder von Deepkit ORM unterstützten Datenbank verwendet werden. + +![ORM Browser](/assets/screenshots-orm-browser/content-editing.png) + +## Installation + +Deepkit ORM Browser ist Teil des Deepkit Frameworks und wird aktiviert, wenn der Debug-Modus eingeschaltet ist. + +```typescript +import { App } from '@deepkit/app'; +import { Database } from '@deepkit/orm'; + +class MyController { + @http.GET('/') + index() { + return 'Hello World'; + } +} + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +new App({ + controllers: [MyController], + providers: [MainDatabase], + imports: [new FrameworkModule({debug: true})], +}).run(); +``` + +Alternativ kannst du Deepkit ORM Browser als eigenständiges Paket installieren. + +```bash +npm install @deepkit/orm-browser +``` + +```typescript +// database.ts +import { Database } from '@deepkit/orm'; + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +export const database = new MainDatabase(); +``` + +Als Nächstes kann der Deepkit ORM Browser-Server gestartet werden. + +```sh +./node_modules/.bin/deepkit-orm-browser database.ts +``` + +Deepkit ORM Browser ist nun unter http://localhost:9090 verfügbar. \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/plugin-soft-delete.md b/website/src/translations/de/documentation/orm/plugin-soft-delete.md new file mode 100644 index 000000000..26a5e4286 --- /dev/null +++ b/website/src/translations/de/documentation/orm/plugin-soft-delete.md @@ -0,0 +1,112 @@ +# Soft-Delete + +Das Soft-Delete-Plugin ermöglicht es, Datenbankeinträge verborgen zu halten, ohne sie tatsächlich zu löschen. Wenn ein Eintrag gelöscht wird, wird er nur als gelöscht markiert und nicht wirklich entfernt. Alle Abfragen filtern diese deleted-Property automatisch, sodass es sich für den Benutzer so anfühlt, als wäre er tatsächlich gelöscht. + +Um das Plugin zu verwenden, müssen Sie die SoftDelete Class instanziieren und sie für jede Entity aktivieren. + +```typescript +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { SoftDelete } from '@deepkit/orm'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + // Dieses Feld dient als Indikator dafür, ob der Datensatz soft gelöscht ist. + // Wenn es gesetzt ist, ist der Datensatz soft gelöscht. + deletedAt?: Date; + + // Dieses Feld ist optional und kann verwendet werden, um nachzuverfolgen, wer/was den Datensatz gelöscht hat. + deletedBy?: string; + + constructor( + public name: string + ) { + } +} + +const softDelete = new SoftDelete(database); +softDelete.enable(User); + +// oder wieder deaktivieren +softDelete.disable(User); +``` + +## Löschen + +Um Datensätze soft zu löschen, verwenden Sie die üblichen Methoden: `deleteOne` oder `deleteMany` in einer Abfrage, oder verwenden Sie die Session, um sie zu löschen. Das Soft-Delete-Plugin erledigt den Rest automatisch im Hintergrund. + +## Wiederherstellen + +Gelöschte Datensätze können mit einer "lifted query" über `SoftDeleteQuery` wiederhergestellt werden. Sie bietet `restoreOne` und `restoreMany`. + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreOne(); +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreMany(); +``` + +Die Session unterstützt ebenfalls das Wiederherstellen von Elementen. + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).restore(user1); +await session.commit(); +``` + +## Hard Delete + +Um Datensätze hart zu löschen, verwenden Sie eine "lifted query" über SoftDeleteQuery. Dies stellt im Wesentlichen das normale Verhalten wieder her, bei dem kein Soft-Delete-Plugin verwendet wird. + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// den Datensatz wirklich aus der Datenbank löschen +await database.query(User).lift(SoftDeleteQuery).hardDeleteOne(); +await database.query(User).lift(SoftDeleteQuery).hardDeleteMany(); + +// diese sind äquivalent zu den obigen +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteOne(); +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteMany(); +``` + +## Gelöschte abfragen. + +Mit einer "lifted" Query über `SoftDeleteQuery` können Sie auch gelöschte Datensätze einbeziehen. + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// alle finden, soft gelöscht und nicht gelöscht +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().find(); + +// nur soft gelöschte finden +await database.query(s).lift(SoftDeleteQuery).isSoftDeleted().count() +``` + +## Gelöscht von + +`deletedBy` kann über Abfragen und Sessions gesetzt werden. + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).setDeletedBy('Peter'); +session.remove(user1); + +await session.commit(); +import { SoftDeleteQuery } from '@deepkit/orm'; + +database.query(User).lift(SoftDeleteQuery) +.deletedBy('Peter') +.deleteMany(); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/query.md b/website/src/translations/de/documentation/orm/query.md new file mode 100644 index 000000000..18ae9a66d --- /dev/null +++ b/website/src/translations/de/documentation/orm/query.md @@ -0,0 +1,357 @@ +# Query + +Eine Query ist ein Objekt, das beschreibt, wie Daten aus der Datenbank abgerufen oder verändert werden. Sie hat mehrere Methoden, um die Query zu beschreiben, und Abschlussmethoden, die sie ausführen. Der Database Adapter kann die Query-API auf viele Arten erweitern, um datenbankspezifische Features zu unterstützen. + +Eine Query kann mit `Database.query(T)` oder `Session.query(T)` erstellt werden. Wir empfehlen Sessions, da dies die Performance verbessert. + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + birthdate?: Date; + visits: number = 0; + + constructor(public username: string) { + } +} + +const database = new Database(...); + +//[ { username: 'User1' }, { username: 'User2' }, { username: 'User2' } ] +const users = await database.query(User).select('username').find(); +``` + +## Filter + +Ein Filter kann angewendet werden, um die Ergebnismenge einzuschränken. + +```typescript +//einfache Filter +const users = await database.query(User).filter({name: 'User1'}).find(); + +//mehrere Filter, alle AND +const users = await database.query(User).filter({name: 'User1', id: 2}).find(); + +//Bereichsfilter: $gt, $lt, $gte, $lte (größer als, kleiner als, ...) +//entspricht WHERE created < NOW() +const users = await database.query(User).filter({created: {$lt: new Date}}).find(); +//entspricht WHERE id > 500 +const users = await database.query(User).filter({id: {$gt: 500}}).find(); +//entspricht WHERE id >= 500 +const users = await database.query(User).filter({id: {$gte: 500}}).find(); + +//Set-Filter: $in, $nin (in, not in) +//entspricht WHERE id IN (1, 2, 3) +const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find(); + +//Regex-Filter +const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find(); + +//Gruppierung: $and, $nor, $or +//entspricht WHERE (username = 'User1') OR (username = 'User2') +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2'}] +}).find(); + + +//verschachtelte Gruppierung +//entspricht WHERE username = 'User1' OR (username = 'User2' and id > 0) +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2', id: {$gt: 0}}] +}).find(); + + +//verschachtelte Gruppierung +//entspricht WHERE username = 'User1' AND (created < NOW() OR id > 0) +const users = await database.query(User).filter({ + $and: [{username: 'User1'}, {$or: [{created: {$lt: new Date}, id: {$gt: 0}}]}] +}).find(); +``` + +### Equal + +### Greater / Smaller + +### RegExp + +### Grouping AND/OR + +### In + +## Select + +Um die Felder einzugrenzen, die aus der Datenbank abgerufen werden, kann `select('field1')` verwendet werden. + +```typescript +const user = await database.query(User).select('username').findOne(); +const user = await database.query(User).select('id', 'username').findOne(); +``` + +Wichtig ist: Sobald die Felder mit `select` eingegrenzt werden, sind die Ergebnisse keine Instanzen der Entity mehr, sondern nur noch Object-Literals. + +``` +const user = await database.query(User).select('username').findOne(); +user instanceof User; //false +``` + +## Order + +Mit `orderBy(field, order)` kann die Reihenfolge der Einträge geändert werden. +`orderBy` kann mehrfach ausgeführt werden, um die Sortierung immer weiter zu verfeinern. + +```typescript +const users = await session.query(User).orderBy('created', 'desc').find(); +const users = await session.query(User).orderBy('created', 'asc').find(); +``` + +## Pagination + +Die Methoden `itemsPerPage()` und `page()` können verwendet werden, um die Ergebnisse zu paginieren. Die Page beginnt bei 1. + +```typescript +const users = await session.query(User).itemsPerPage(50).page(1).find(); +``` + +Mit den alternativen Methoden `limit` und `skip` kann man manuell paginieren. + +```typescript +const users = await session.query(User).limit(5).skip(10).find(); +``` + +[#database-join] +## Join + +Standardmäßig werden References aus der Entity weder in Queries berücksichtigt noch geladen. Um einen Join in die Query einzuschließen, ohne die Reference zu laden, verwenden Sie `join()` (Left Join) oder `innerJoin()`. Um einen Join in die Query einzuschließen und die Reference zu laden, verwenden Sie `joinWith()` oder `innerJoinWith()`. + +Alle folgenden Beispiele gehen von diesen Model-Schemas aus: + +```typescript +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + group?: Group & Reference; + + constructor(public username: string) { + } +} +``` + +```typescript +//wählt nur Users mit einer zugewiesenen group (INNER JOIN) +const users = await session.query(User).innerJoin('group').find(); +for (const user of users) { + user.group; //Fehler, da die Referenz nicht geladen wurde +} +``` + +```typescript +//wählt nur Users mit einer zugewiesenen group (INNER JOIN) und lädt die Relation +const users = await session.query(User).innerJoinWith('group').find(); +for (const user of users) { + user.group.name; //funktioniert +} +``` + +Um Join-Queries zu modifizieren, verwenden Sie dieselben Methoden, jedoch mit dem Prefix `use`: `useJoin`, `useInnerJoin`, `useJoinWith` oder `useInnerJoinWith`. Um die Join-Query-Modifikation zu beenden, verwenden Sie `end()`, um zur Parent-Query zurückzukehren. + +```typescript +//wählt nur Users mit einer group mit dem Namen 'admins' (INNER JOIN) +const users = await session.query(User) + .useInnerJoinWith('group') + .filter({name: 'admins'}) + .end() // kehrt zur Parent-Query zurück + .find(); + +for (const user of users) { + user.group.name; //immer admin +} +``` + +## Aggregation + +Aggregation-Methoden ermöglichen es, Records zu zählen und Felder zu aggregieren. + +Die folgenden Beispiele gehen von diesem Model-Schema aus: + +```typescript +@entity.name('file') +class File { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + downloads: number = 0; + + category: string = 'none'; + + constructor(public path: string & Index) { + } +} +``` + +`groupBy` ermöglicht es, das Ergebnis nach dem angegebenen Feld zu gruppieren. + +```typescript +await database.persist( + cast({path: 'file1', category: 'images'}), + cast({path: 'file2', category: 'images'}), + cast({path: 'file3', category: 'pdfs'}) +); + +//[ { category: 'images' }, { category: 'pdfs' } ] +await session.query(File).groupBy('category').find(); +``` + +Es gibt mehrere Aggregation-Methoden: `withSum`, `withAverage`, `withCount`, `withMin`, `withMax`, `withGroupConcat`. Jede benötigt einen Feldnamen als erstes Argument und optional ein zweites Argument, um den Alias zu ändern. + +```typescript +//zuerst aktualisieren wir einige Records: +await database.query(File).filter({path: 'images/file1'}).patchOne({$inc: {downloads: 15}}); +await database.query(File).filter({path: 'images/file2'}).patchOne({$inc: {downloads: 5}}); + +//[{ category: 'images', downloads: 20 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withSum('downloads').find(); + +//[{ category: 'images', downloads: 10 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withAverage('downloads').find(); + +//[ { category: 'images', amount: 2 }, { category: 'pdfs', amount: 1 } ] +await session.query(File).groupBy('category').withCount('id', 'amount').find(); +``` + +## Returning + +Mit `returning` können bei Änderungen über `patch` und `delete` zusätzliche Felder angefordert werden. + +Achtung: Nicht alle Database Adapter liefern Felder atomar zurück. Verwenden Sie Transactions, um Datenkonsistenz sicherzustellen. + +```typescript +await database.query(User).patchMany({visits: 0}); + +//{ modified: 1, returning: { visits: [ 5 ] }, primaryKeys: [ 1 ] } +const result = await database.query(User) + .filter({username: 'User1'}) + .returning('username', 'visits') + .patchOne({$inc: {visits: 5}}); +``` + +## Find + +Gibt ein Array von Einträgen zurück, die dem angegebenen Filter entsprechen. + +```typescript +const users: User[] = await database.query(User).filter({username: 'Peter'}).find(); +``` + +## FindOne + +Gibt einen Eintrag zurück, der dem angegebenen Filter entspricht. +Wenn kein Eintrag gefunden wird, wird ein `ItemNotFound` Error ausgelöst. + +```typescript +const users: User = await database.query(User).filter({username: 'Peter'}).findOne(); +``` + +## FindOneOrUndefined + +Gibt einen Eintrag zurück, der dem angegebenen Filter entspricht. +Wenn kein Eintrag gefunden wird, wird undefined zurückgegeben. + +```typescript +const query = database.query(User).filter({username: 'Peter'}); +const users: User|undefined = await query.findOneOrUndefined(); +``` + +## FindField + +Gibt eine Liste eines Feldes zurück, die dem angegebenen Filter entspricht. + +```typescript +const usernames: string[] = await database.query(User).findField('username'); +``` + +## FindOneField + +Gibt eine Liste eines Feldes zurück, die dem angegebenen Filter entspricht. +Wenn kein Eintrag gefunden wird, wird ein `ItemNotFound` Error ausgelöst. + +```typescript +const username: string = await database.query(User).filter({id: 3}).findOneField('username'); +``` + +## Patch + +Patch ist eine Change-Query, die die in der Query beschriebenen Records patcht. Die Methoden +`patchOne` und `patchMany` beenden die Query und führen den Patch aus. + +`patchMany` ändert alle Records in der Datenbank, die dem angegebenen Filter entsprechen. Wenn kein Filter gesetzt ist, wird die gesamte Tabelle geändert. Verwenden Sie `patchOne`, um jeweils nur einen Eintrag zu ändern. + +```typescript +await database.query(User).filter({username: 'Peter'}).patch({username: 'Peter2'}); + +await database.query(User).filter({username: 'User1'}).patchOne({birthdate: new Date}); +await database.query(User).filter({username: 'User1'}).patchOne({$inc: {visits: 1}}); + +await database.query(User).patchMany({visits: 0}); +``` + +## Delete + +`deleteMany` löscht alle Einträge in der Datenbank, die dem angegebenen Filter entsprechen. +Wenn kein Filter gesetzt ist, wird die gesamte Tabelle gelöscht. Verwenden Sie `deleteOne`, um jeweils nur einen Eintrag zu löschen. + +```typescript +const result = await database.query(User) + .filter({visits: 0}) + .deleteMany(); + +const result = await database.query(User).filter({id: 4}).deleteOne(); +``` + +## Has + +Gibt zurück, ob mindestens ein Eintrag in der Datenbank existiert. + +```typescript +const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has(); +``` + +## Count + +Gibt die Anzahl der Einträge zurück. + +```typescript +const userCount: number = await database.query(User).count(); +``` + +## Lift + +Eine Query zu liften bedeutet, ihr neue Funktionalität hinzuzufügen. Dies wird üblicherweise entweder von Plugins oder in komplexen Architekturen verwendet, um größere Query-Klassen in mehrere bequeme, wiederverwendbare Klassen aufzuteilen. + +```typescript +import { FilterQuery, Query } from '@deepkit/orm'; + +class UserQuery extends Query { + hasBirthday() { + const start = new Date(); + start.setHours(0,0,0,0); + const end = new Date(); + end.setHours(23,59,59,999); + + return this.filter({$and: [{birthdate: {$gte: start}}, {birthdate: {$lte: end}}]} as FilterQuery); + } +} + +await session.query(User).lift(UserQuery).hasBirthday().find(); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/raw-access.md b/website/src/translations/de/documentation/orm/raw-access.md new file mode 100644 index 000000000..030424f5d --- /dev/null +++ b/website/src/translations/de/documentation/orm/raw-access.md @@ -0,0 +1,78 @@ +# Rohzugriff + +Es ist oft notwendig, direkt auf die Datenbank zuzugreifen, z. B. um eine SQL-Query auszuführen, die vom ORM nicht unterstützt wird. +Das kann über die `raw` Method der `Database` Class erfolgen. + +```typescript +import { PrimaryKey, AutoIncrement, @entity } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; +import { sql } from '@deepkit/sql'; +import { SqliteDatabaseAdapter } from '@deepkit/sqlite'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + constructor(public username: string) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + +const query = 'Pet%'; +const rows = await database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`).find(); + +const result = await database.raw<{ count: number }>(sql`SELECT count(*) as count FROM users WHERE username LIKE ${query}`).findOne(); +console.log('Found', result.count, 'users'); +``` + +Die SQL-Query wird mit dem `sql` Template-String-Tag aufgebaut. Dies ist ein spezielles Template-String-Tag, das erlaubt, Werte als Parameter zu übergeben. Diese Parameter werden dann automatisch geparst und in ein sicheres Prepared Statement konvertiert. Das ist wichtig, um SQL-Injection-Angriffe zu vermeiden. + +Um einen dynamischen Identifier wie einen Spaltennamen zu übergeben, kann `identifier` verwendet werden: + +```typescript +import { identifier, sql } from '@deepkit/sql'; + +let column = 'username'; +const rows = await database.raw(sql`SELECT * FROM users WHERE ${identifier(column)} LIKE ${query}`).find(); +``` + +Für SQL-Adapter gibt die `raw` Method eine `RawQuery` mit den `findOne`- und `find`-Methods zurück, um die Resultate abzurufen. Um eine SQL-Anweisung ohne Rückgabe von Zeilen auszuführen, wie UPDATE/DELETE/etc., kann `execute` verwendet werden: + +```typescript +let username = 'Peter'; +await database.raw(sql`UPDATE users SET username = ${username} WHERE id = 1`).execute(); +``` + +`RawQuery` unterstützt außerdem das Auslesen des finalen SQL-Strings und der Parameter, korrekt formatiert für den Datenbank-Adapter: + +```typescript +const query = database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`); +console.log(query.sql); +console.log(query.params); +``` + +Auf diese Weise kann das SQL z. B. in einem anderen Datenbank-Client ausgeführt werden. + +## Types + +Beachte, dass an `raw` jeder Type übergeben werden kann und das Resultat aus der Datenbank automatisch in diesen Type konvertiert wird. Das ist besonders nützlich für SQL-Adapter, bei denen eine Class übergeben werden kann und das Resultat automatisch in diese Class konvertiert wird. + +Das hat jedoch Einschränkungen. SQL-Joins werden auf diese Weise nicht unterstützt. Wenn Joins verwendet werden sollen, muss der Query Builder des ORM genutzt werden. + +## Mongo + +Der MongoDB-Adapter funktioniert etwas anders, da er nicht auf SQL-Queries basiert, sondern auf Mongo-Commands. + +Ein Command kann eine Aggregation-Pipeline, eine find-Query oder ein Write-Command sein. + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase')); + +// Erstes Argument ist die Entry-Point-Collection, zweites ist der Return Type des Commands +const items = await database.raw([ + { $match: { roomId: 'room1' } }, + { $group: { _id: '$userId', count: { $sum: 1 } } }, +]).find(); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/relations.md b/website/src/translations/de/documentation/orm/relations.md new file mode 100644 index 000000000..3301bd711 --- /dev/null +++ b/website/src/translations/de/documentation/orm/relations.md @@ -0,0 +1,153 @@ +# Relationen + +Beziehungen ermöglichen es, zwei Entities auf bestimmte Weise zu verbinden. Dies geschieht in Datenbanken üblicherweise mithilfe des Konzepts der Foreign Keys. Deepkit ORM unterstützt Relationen für alle offiziellen Database Adapter. + +Eine Relation wird mit dem `Reference` Decorator annotiert. Üblicherweise hat eine Relation auch eine umgekehrte Relation, die mit dem `BackReference` Type annotiert wird, aber nur benötigt wird, wenn die umgekehrte Relation in einer Database Query verwendet werden soll. Back References sind nur virtuell. + +## Eins-zu-Viele + +Die Entity, die eine Referenz speichert, wird üblicherweise als die `owning side` bzw. diejenige bezeichnet, die die Referenz `owns`. Der folgende Code zeigt zwei Entities mit einer Eins-zu-Viele-Beziehung zwischen `User` und `Post`. Das bedeutet, dass ein `User` mehrere `Post` haben kann. Die `post`-Entity hat die Relation `post->user`. In der Datenbank selbst gibt es nun ein Feld `Post. "author"`, das den Primary Key von `User` enthält. + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement, + Reference } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.collection('posts') +class Post { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor( + public author: User & Reference, + public title: string + ) { + } +} + +const database = new Database( + new SQLiteDatabaseAdapter(':memory:'), + [User, Post] +); +await database.migrate(); + +const user1 = new User('User1'); +const post1 = new Post(user1, 'My first blog post'); +const post2 = new Post(user1, 'My second blog post'); + +await database.persist(user1, post1, post2); +``` + +Referenzen werden in Queries standardmäßig nicht ausgewählt. Siehe [Datenbank-Joins](./query.md#join) für Details. + +## Viele-zu-Eins + +Eine Referenz hat üblicherweise eine umgekehrte Referenz (Viele-zu-Eins). Sie ist nur eine virtuelle Referenz, da sie sich nicht in der Datenbank selbst widerspiegelt. Eine Back Reference wird mit `BackReference` annotiert und wird hauptsächlich für Reflection und Query Joins verwendet. Wenn Sie eine `BackReference` von `User` zu `Post` hinzufügen, können Sie `Post` direkt aus `User`-Queries joinen. + +```typescript +@entity.name('user').collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + posts?: Post[] & BackReference; + + constructor(public username: string) { + } +} +``` + +```typescript +//[ { username: 'User1', posts: [ [Post], [Post] ] } ] +const users = await database.query(User) + .select('username', 'posts') + .joinWith('posts') + .find(); +``` + +## Viele-zu-Viele + +Eine Viele-zu-Viele-Beziehung ermöglicht es, viele Datensätze mit vielen anderen zu verknüpfen. Sie kann beispielsweise für Users in Groups verwendet werden. Ein User kann in keiner, einer oder vielen Groups sein. Folglich kann eine Group 0, eine oder viele Users enthalten. + +Viele-zu-Viele-Beziehungen werden üblicherweise mithilfe einer Pivot-Entity implementiert. Die Pivot-Entity enthält die tatsächlichen eigenen Referenzen zu zwei anderen Entities, und diese beiden Entities haben Back References zur Pivot-Entity. + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + groups?: Group[] & BackReference<{via: typeof UserGroup}>; + + constructor(public username: string) { + } +} + +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + + users?: User[] & BackReference<{via: typeof UserGroup}>; + + constructor(public name: string) { + } +} + +//die Pivot-Entity +@entity.name('userGroup') +class UserGroup { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public user: User & Reference, + public group: Group & Reference, + ) { + } +} +``` + +Mit diesen Entities können Sie nun Users und Groups erstellen und sie über die Pivot-Entity miteinander verbinden. Durch die Verwendung einer Back Reference in User können wir die Groups direkt mit einer User-Query abrufen. + +```typescript +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Group, UserGroup]); +await database.migrate(); + +const user1 = new User('User1'); +const user2 = new User('User2'); +const group1 = new Group('Group1'); + +await database.persist(user1, user2, group1, new UserGroup(user1, group1), new UserGroup(user2, group1)); + +//[ +// { id: 1, username: 'User1', groups: [ [Group] ] }, +// { id: 2, username: 'User2', groups: [ [Group] ] } +// ] +const users = await database.query(User) + .select('username', 'groups') + .joinWith('groups') + .find(); +``` + +Um die Verknüpfung eines Users mit einer Group aufzuheben, wird der UserGroup-Datensatz gelöscht: + +```typescript +const users = await database.query(UserGroup) + .filter({user: user1, group: group1}) + .deleteOne(); +``` + +## Eins-zu-Eins + +## Constraints + +Bei Delete/Update: RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/seeding.md b/website/src/translations/de/documentation/orm/seeding.md new file mode 100644 index 000000000..fce6ca1b1 --- /dev/null +++ b/website/src/translations/de/documentation/orm/seeding.md @@ -0,0 +1,3 @@ +# Seeding + +Datenbank-Seeding ist die initiale Befüllung einer Datenbank mit Daten. Es ist eine Form der anfänglichen Datenbankbefüllung, vorgesehen für die Verwendung während der Entwicklung, beim Testen oder bei der Initialisierung einer Produktionsdatenbank. \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/session.md b/website/src/translations/de/documentation/orm/session.md new file mode 100644 index 000000000..a2458175a --- /dev/null +++ b/website/src/translations/de/documentation/orm/session.md @@ -0,0 +1,59 @@ +# Session / Unit Of Work + +Eine Session ist so etwas wie eine Unit of Work. Sie verfolgt alles, was Sie tun, und zeichnet die Änderungen automatisch auf, sobald `commit()` aufgerufen wird. Sie ist der bevorzugte Weg, Änderungen in der Datenbank auszuführen, da sie Statements so bündelt, dass die Ausführung sehr schnell ist. Eine Session ist sehr leichtgewichtig und kann beispielsweise leicht in einem Request-Response-Lifecycle erstellt werden. + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + + @entity.name('user') + class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + await database.migrate(); + + const session = database.createSession(); + session.add(new User('User1'), new User('User2'), new User('User3')); + + await session.commit(); + + const users = await session.query(User).find(); + console.log(users); +} + +main(); +``` + +Fügen Sie eine neue Instanz mit `session.add(T)` zur Session hinzu oder entfernen Sie bestehende Instanzen mit `session.remove(T)`. Sobald Sie mit dem Session-Objekt fertig sind, entfernen Sie einfach überall die Referenzen darauf, damit der Garbage Collector es entfernen kann. + +Änderungen werden für über das Session-Objekt geladene Entity-Instanzen automatisch erkannt. + +```typescript +const users = await session.query(User).find(); +for (const user of users) { + user.name += ' changed'; +} + +await session.commit();//speichert alle User +``` + +## Identity Map + +Sessions stellen eine Identity Map bereit, die sicherstellt, dass es pro Datenbankeintrag nur genau ein JavaScript-Objekt gibt. Wenn Sie z. B. `session.query(User).find()` zweimal innerhalb derselben Session ausführen, erhalten Sie zwei unterschiedliche Arrays, jedoch mit denselben Entity-Instanzen darin. + +Wenn Sie mit `session.add(entity1)` eine neue Entity hinzufügen und sie erneut abrufen, erhalten Sie genau dieselbe Entity-Instanz `entity1`. + +Wichtig: Sobald Sie Sessions verwenden, sollten Sie deren `session.query`-Methode statt `database.query` verwenden. Nur Session-Queries haben das Identity-Mapping-Feature aktiviert. + +## Change Detection + +## Request/Response \ No newline at end of file diff --git a/website/src/translations/de/documentation/orm/transactions.md b/website/src/translations/de/documentation/orm/transactions.md new file mode 100644 index 000000000..f01984402 --- /dev/null +++ b/website/src/translations/de/documentation/orm/transactions.md @@ -0,0 +1,85 @@ +# Transaktionen + +Eine Transaktion ist eine sequentielle Gruppe von Statements, Queries oder Operationen wie select, insert, update oder delete, die als eine einzige Unit-of-Work ausgeführt wird und die entweder committed oder zurückgerollt (rolled back) werden kann. + +Deepkit unterstützt Transaktionen für alle offiziell unterstützten Datenbanken. Standardmäßig werden für keine Abfrage (Query) oder Datenbank-Session Transaktionen verwendet. Um Transaktionen zu aktivieren, gibt es zwei Hauptmethoden: Sessions und Callback. + +## Session-Transaktionen + +Sie können für jede erstellte Session eine neue Transaktion starten und zuweisen. Dies ist die bevorzugte Art der Interaktion mit der Datenbank, da Sie das Session-Objekt leicht weitergeben können und alle von dieser Session instanziierten Queries automatisch ihrer Transaktion zugeordnet werden. + +Ein typisches Muster ist, alle Operationen in einen try-catch-Block zu kapseln und `commit()` in der allerletzten Zeile auszuführen (das nur ausgeführt wird, wenn alle vorherigen Befehle erfolgreich waren) und `rollback()` im catch-Block, um alle Änderungen zurückzurollen, sobald ein Fehler auftritt. + +Obwohl es eine alternative API gibt (siehe unten), funktionieren alle Transaktionen nur mit Datenbank-Session-Objekten. Um offene Änderungen der Unit-of-Work in einer Datenbank-Session an die Datenbank zu committen, wird normalerweise `commit()` aufgerufen. In einer transaktionalen Session committet `commit()` nicht nur alle ausstehenden Änderungen an die Datenbank, sondern schließt auch die Transaktion ab ("commits" sie) und beendet damit die Transaktion. Alternativ können Sie `session.flush()` aufrufen, um alle ausstehenden Änderungen ohne `commit` und somit ohne die Transaktion zu schließen zu committen. Um eine Transaktion zu committen, ohne die Unit-of-Work zu flushen, verwenden Sie `session.commitTransaction()`. + +```typescript +const session = database.createSession(); + +//weist eine neue Transaktion zu und startet sie mit der nächsten Datenbankoperation. +session.useTransaction(); + +try { + //diese Query wird in der Transaktion ausgeführt + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); + + await session.commit(); +} catch (error) { + await session.rollback(); +} +``` + +Sobald `commit()` oder `rollback()` in einer Session ausgeführt wurde, wird die Transaktion freigegeben. Sie müssen dann erneut `useTransaction()` aufrufen, wenn Sie in einer neuen Transaktion fortfahren möchten. + +Bitte beachten Sie, dass sobald die erste Datenbankoperation in einer transaktionalen Session ausgeführt wird, die zugewiesene Datenbankverbindung fest und exklusiv dem aktuellen Session-Objekt zugeordnet wird (sticky). Somit werden alle nachfolgenden Operationen über dieselbe Verbindung (und damit in den meisten Datenbanken auf demselben Datenbankserver) ausgeführt. Erst wenn die transaktionale Session beendet wird (commit oder rollback), wird die Datenbankverbindung wieder freigegeben. Es wird daher empfohlen, eine Transaktion nur so kurz wie nötig zu halten. + +Wenn eine Session bereits mit einer Transaktion verbunden ist, gibt ein Aufruf von `session.useTransaction()` immer dasselbe Objekt zurück. Verwenden Sie `session.isTransaction()`, um zu prüfen, ob der Session eine Transaktion zugeordnet ist. + +Verschachtelte Transaktionen werden nicht unterstützt. + +## Transaktions-Callback + +Eine Alternative zu transaktionalen Sessions ist `database.transaction(callback)`. + +```typescript +await database.transaction(async (session) => { + //diese Query wird in der Transaktion ausgeführt + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); +}); +``` + +Die Methode `database.transaction(callback)` führt einen asynchronen Callback innerhalb einer neuen transaktionalen Session aus. Wenn der Callback erfolgreich ist (d. h. kein Fehler geworfen wird), wird die Session automatisch committed (und damit ihre Transaktion committed und alle Änderungen geflusht). Wenn der Callback fehlschlägt, führt die Session automatisch `rollback()` aus und der Fehler wird propagiert. + +## Isolationsebenen + +Viele Datenbanken unterstützen unterschiedliche Arten von Transaktionen. Um das Transaktionsverhalten zu ändern, können Sie verschiedene Methoden für das von `useTransaction()` zurückgegebene Transaktionsobjekt aufrufen. Das Interface dieses Transaktionsobjekts hängt vom verwendeten Datenbank-Adapter ab. Das von einer MySQL-Datenbank zurückgegebene Transaktionsobjekt hat beispielsweise andere Optionen als das von einer MongoDB-Datenbank zurückgegebene. Verwenden Sie Code Completion oder sehen Sie sich das Interface des Datenbank-Adapters an, um eine Liste möglicher Optionen zu erhalten. + +```typescript +const database = new Database(new MySQLDatabaseAdapter()); + +const session = database.createSession(); +session.useTransaction().readUncommitted(); + +try { + //...Operationen + await session.commit(); +} catch (error) { + await session.rollback(); +} + +//oder +await database.transaction(async (session) => { + //dies funktioniert, solange noch keine Datenbankoperation ausgeführt wurde. + session.useTransaction().readUncommitted(); + + //...Operationen +}); +``` + +Während Transaktionen für MySQL, PostgreSQL und SQLite standardmäßig funktionieren, müssen Sie MongoDB zuerst als "Replica Set" einrichten. + +Um eine Standard-MongoDB-Instanz in ein Replica Set zu konvertieren, lesen Sie bitte den Link zur offiziellen Dokumentation: +[Eine Standalone-Instanz in ein Replica Set umwandeln](https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set). \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/angular-ssr.md b/website/src/translations/de/documentation/package/angular-ssr.md new file mode 100644 index 000000000..b5403b25b --- /dev/null +++ b/website/src/translations/de/documentation/package/angular-ssr.md @@ -0,0 +1,128 @@ +# API `@deepkit/angular-ssr` + +```shell +npm install @deepkit/angular-ssr +``` + + +- Stellen Sie sicher, dass sich Ihre Hauptanwendung in `app.ts` befindet und konfigurieren Sie sie in Ihrer `angular.json`: + +In `src/server/app.ts`: + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { AngularModule, RequestHandler } from '@deepkit/angular-ssr'; + +const app = new App({ + controllers: [ + // Ihre Controllers + ], + providers: [ + // Ihre Providers + ], + imports: [ + new FrameworkModule({}), + new AngularModule({ + moduleUrl: import.meta.url, + }) + ] +}); + +const main = isMainModule(import.meta.url); + +if (main) { + void app.run(); //ermöglicht das Ausführen aller CLI-Befehle, einschließlich server:start +} + +export const reqHandler = main + //wenn im main, möchten wir keinen neuen Request-Handler erstellen + ? () => undefined + : app.get(RequestHandler).create(); +``` + +```json +{ + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/app", + "index": "src/index.html", + "server": "src/main.server.ts", + "outputMode": "server", + "ssr": { + "entry": "src/server/app.ts" + }, + "browser": "src/main.ts" + } +} +``` + +Stellen Sie sicher, dass sich `src/server/app.ts` ebenfalls in Ihrer tsconfig befindet. + +## Angular-App konfigurieren + +In `app/app.config.ts` (Client-Seite): + +```typescript +@Injectable() +export class APIInterceptor implements HttpInterceptor { + constructor(@Inject('baseUrl') @Optional() private baseUrl: string) { + // Im Client-Build ist `baseUrl` leer und sollte aus der aktuellen Location abgeleitet werden. + // Falls das nicht korrekt ist, können Sie die `baseUrl` einfach im `providers`-Array des `appConfig`-Objekts definieren. + this.baseUrl = baseUrl || (typeof location !== 'undefined' ? location.origin : ''); + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const apiReq = req.clone({ url: `${this.baseUrl}/${req.url}` }); + return next.handle(apiReq); + } +} + +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: APIInterceptor, + multi: true, + }, + // Ihre weiteren Providers + ], +}; +``` + +In `app/app.server.config.ts` (Server-Seite): + +```typescript +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering(), + provideServerRouting([ + { + path: '**', + renderMode: RenderMode.Server, + }, + ]), + { + provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP, + deps: [REQUEST_CONTEXT], + useFactory(context: any) { + return { [context?.baseUrl]: context?.publicBaseUrl || '' }; + }, + }, + { + provide: 'baseUrl', + deps: [REQUEST_CONTEXT], + useFactory: (context: any) => { + return context?.baseUrl || ''; + }, + } + ], +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); +``` + + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/api-console.md b/website/src/translations/de/documentation/package/api-console.md new file mode 100644 index 000000000..9f62af0e8 --- /dev/null +++ b/website/src/translations/de/documentation/package/api-console.md @@ -0,0 +1,40 @@ +# Deepkit API Console + +```bash +npms install @deepkit/api-console-module +``` + +Automatische Dokumentation der HTTP- und RPC-API, die alle routes, actions, parameters, return types und status codes in TypeScript type syntax anzeigt. + +Es ist Teil des [Framework Debugger](../framework.md), kann aber auch eigenständig verwendet werden. + +```typescript +import { ApiConsoleModule } from '@deepkit/api-console-module'; + +new App({ + imports: [ + new ApiConsoleModule({ + path: '/api', + markdown: ` + # Meine API + + Dies ist meine API-Dokumentation. + + Viel Spaß! + ` + }), + ] +}) +``` + +Standardmäßig zeigt `new ApiConsoleModule` alle HTTP- und RPC-routes an. Sie können außerdem festlegen, welche routes angezeigt werden sollen, indem Sie die Methoden der `ApiConsoleModule` Class verwenden. + + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/app.md b/website/src/translations/de/documentation/package/app.md new file mode 100644 index 000000000..3c175b30d --- /dev/null +++ b/website/src/translations/de/documentation/package/app.md @@ -0,0 +1,14 @@ +# API `@deepkit/app` + +```shell +npm install @deepkit/app +``` + +Stellt einen Command-Line-Interface-Parser, einen Service-Container und Dependency Injection, einen Event-Dispatcher, ein App-Module-System sowie einen Configuration Loader bereit. + +Dies ist der Kern zum Schreiben von Deepkit-Anwendungen. +Sie können CLI-Controller, HTTP-Controller oder Routen, RPC-Controller (über das [Framework-Modul](../framework.md)) und andere Services registrieren. + +Weitere Informationen finden Sie in der [Deepkit-App-Dokumentation](../app.md). + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/bench.md b/website/src/translations/de/documentation/package/bench.md new file mode 100644 index 000000000..ece14988a --- /dev/null +++ b/website/src/translations/de/documentation/package/bench.md @@ -0,0 +1,35 @@ +# API `@deepkit/bench` + +```sh +npm install @deepkit/bench +``` + +Ein einfaches Tool zum Benchmarken von Code-Snippets. + +```typescript +import { benchmark, run } from '@deepkit/bench'; + +// Beispiel für ASCII-Binär-Parsing +const binaryString = Buffer.from('Hello World', 'utf8'); +const codes = [ + +benchmark('Buffer.toString', () => { + const utf8String = binaryString.toString('utf8'); +}); + +benchmark('String.fromCodePoint', () => { + const utf8String = String.fromCodePoint() +}); + +void run(); +``` + +```sh +$ node --import @deepkit/run benchmarks/ascii-parsing.ts +Node v22.13.1 + 🏎 x 20,326,482.53 ops/sec ± 4.95% 0.000049 ms/op ▆▆▇▅▆▆▅▅▆▅▅▅▅▅▅▅▅▅▅▅▅▅ Buffer.toString 19850001 samples + 🏎 x 36,012,545.69 ops/sec ± 1.78% 0.000028 ms/op ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ String.fromCodePoint 35800001 samples +done +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/broker-redis.md b/website/src/translations/de/documentation/package/broker-redis.md new file mode 100644 index 000000000..364b10684 --- /dev/null +++ b/website/src/translations/de/documentation/package/broker-redis.md @@ -0,0 +1,29 @@ +# API `@deepkit/broker-redis` + +```sh +npm install @deepkit/broker-redis +``` + +Stellt eine Redis-basierte Implementierung des Deepkit Brokers bereit. Dabei wird ioredis unter der Haube verwendet. + +Dieser Adapter implementiert nicht den Queue-Adapter des Deepkit Brokers. + +```typescript +import { BrokerKeyValue, BrokerBus } from '@deepkit/broker'; +import { BrokerRedisAdapter } from '@deepkit/broker-redis'; +import { ConsoleLogger } from '@deepkit/logger'; + +const adapter = new RedisBrokerAdapter({ + preifx: 'myapp:', + host: 'localhost', + port: 6379, + // Optional, falls der Redis-Server eine Authentifizierung erfordert + // db: 0, // Optional, um eine andere Redis-Datenbank anzugeben +}, new ConsoleLogger()); + +const keyValye = new BrokerKeyValue(adapter); +const bus = new BrokerBus(adapter); +// ... +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/broker.md b/website/src/translations/de/documentation/package/broker.md new file mode 100644 index 000000000..ca5e13639 --- /dev/null +++ b/website/src/translations/de/documentation/package/broker.md @@ -0,0 +1,9 @@ +# API `@deepkit/broker` + +```sh +npm install @deepkit/broker +``` + +Dieses Paket enthält die Abstraktion des Deepkit Brokers sowie eine Server-Implementierung und mehrere Client-Implementierungen als Adapter (`BrokerDeepkitAdapter`, `BrokerMemoryAdapter`). + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/bson.md b/website/src/translations/de/documentation/package/bson.md new file mode 100644 index 000000000..82a66797d --- /dev/null +++ b/website/src/translations/de/documentation/package/bson.md @@ -0,0 +1,9 @@ +# API `@deepkit/bson` + +```shell +npm install @deepkit/bson +``` + +Stellt Encoder/Decoder-Funktionen zur Konvertierung zwischen BSON und JavaScript-Objekten bereit. + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/bun.md b/website/src/translations/de/documentation/package/bun.md new file mode 100644 index 000000000..ebc847bad --- /dev/null +++ b/website/src/translations/de/documentation/package/bun.md @@ -0,0 +1,7 @@ +# API `@deepkit/bun` + +```shell +npm install @deepkit/bun +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/core-rxjs.md b/website/src/translations/de/documentation/package/core-rxjs.md new file mode 100644 index 000000000..bca3c9544 --- /dev/null +++ b/website/src/translations/de/documentation/package/core-rxjs.md @@ -0,0 +1,7 @@ +# API `@deepkit/core-rxjs` + +```shell +npm install @deepkit/core-rxjs +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/core.md b/website/src/translations/de/documentation/package/core.md new file mode 100644 index 000000000..b0e2ccdf0 --- /dev/null +++ b/website/src/translations/de/documentation/package/core.md @@ -0,0 +1,7 @@ +# API `@deepkit/core` + +```shell +npm install @deepkit/core +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/devtool.md b/website/src/translations/de/documentation/package/devtool.md new file mode 100644 index 000000000..cf004841a --- /dev/null +++ b/website/src/translations/de/documentation/package/devtool.md @@ -0,0 +1,5 @@ +# Devtool + +Deepkit Devtool ist ein Chrome-Plugin, das das Debuggen von RPC-Verbindungen in den Chrome DevTools ermöglicht. + +[Deepkit Devtool](https://chromewebstore.google.com/detail/deepkit-devtool/lkncgbbafldohehlfdnkflbeapckdnlj) \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/event.md b/website/src/translations/de/documentation/package/event.md new file mode 100644 index 000000000..2b666bb6d --- /dev/null +++ b/website/src/translations/de/documentation/package/event.md @@ -0,0 +1,9 @@ +# API `@deepkit/event` + +```shell +npm install @deepkit/event +``` + +Lesen Sie [App-Events](../app/events.md) für weitere Informationen darüber, wie Sie Events in Ihrer Anwendung verwenden. + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/filesystem-aws-s3.md b/website/src/translations/de/documentation/package/filesystem-aws-s3.md new file mode 100644 index 000000000..942173a3b --- /dev/null +++ b/website/src/translations/de/documentation/package/filesystem-aws-s3.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-aws-s3` + +```shell +npm install @deepkit/filesystem-aws-s3 +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/filesystem-database.md b/website/src/translations/de/documentation/package/filesystem-database.md new file mode 100644 index 000000000..3f7b5b5ed --- /dev/null +++ b/website/src/translations/de/documentation/package/filesystem-database.md @@ -0,0 +1,9 @@ +# API `@deepkit/filesystem-database` + +```shell +npm install @deepkit/filesystem-database +``` + +Ermöglicht die Verwendung aller unterstützten Deepkit ORM-Datenbanken als Filesystem-Backend. + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/filesystem-ftp.md b/website/src/translations/de/documentation/package/filesystem-ftp.md new file mode 100644 index 000000000..d01521c0a --- /dev/null +++ b/website/src/translations/de/documentation/package/filesystem-ftp.md @@ -0,0 +1,8 @@ +# API `@deepkit/filesystem-ftp` + +```shell +npm install @deepkit/filesystem-ftp +``` + + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/filesystem-google.md b/website/src/translations/de/documentation/package/filesystem-google.md new file mode 100644 index 000000000..c7af80585 --- /dev/null +++ b/website/src/translations/de/documentation/package/filesystem-google.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-google` + +```shell +npm install @deepkit/filesystem-google +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/filesystem-sftp.md b/website/src/translations/de/documentation/package/filesystem-sftp.md new file mode 100644 index 000000000..719ebfd37 --- /dev/null +++ b/website/src/translations/de/documentation/package/filesystem-sftp.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-sftp` + +```shell +npm install @deepkit/filesystem-sftp +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/filesystem.md b/website/src/translations/de/documentation/package/filesystem.md new file mode 100644 index 000000000..68cc87b21 --- /dev/null +++ b/website/src/translations/de/documentation/package/filesystem.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem` + +```shell +npm install @deepkit/filesystem +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/framework.md b/website/src/translations/de/documentation/package/framework.md new file mode 100644 index 000000000..533f18213 --- /dev/null +++ b/website/src/translations/de/documentation/package/framework.md @@ -0,0 +1,8 @@ +# API `@deepkit/framework` + +```shell +npm install @deepkit/framework +``` + + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/http.md b/website/src/translations/de/documentation/package/http.md new file mode 100644 index 000000000..f96adb231 --- /dev/null +++ b/website/src/translations/de/documentation/package/http.md @@ -0,0 +1,8 @@ +# API `@deepkit/http` + +```shell +npm install @deepkit/http +``` + + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/injector.md b/website/src/translations/de/documentation/package/injector.md new file mode 100644 index 000000000..b7b6febab --- /dev/null +++ b/website/src/translations/de/documentation/package/injector.md @@ -0,0 +1,7 @@ +# API `@deepkit/injector` + +```shell +npm install @deepkit/injector +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/logger.md b/website/src/translations/de/documentation/package/logger.md new file mode 100644 index 000000000..224a2be7c --- /dev/null +++ b/website/src/translations/de/documentation/package/logger.md @@ -0,0 +1,7 @@ +# API `@deepkit/logger` + +```sh +npm install @deepkit/logger +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/mongo.md b/website/src/translations/de/documentation/package/mongo.md new file mode 100644 index 000000000..fc77cabff --- /dev/null +++ b/website/src/translations/de/documentation/package/mongo.md @@ -0,0 +1,18 @@ +# API `@deepkit/mongo` + +```shell +npm install @deepkit/mongo +``` + +Eigenständiger MongoDB-Treiber und ein Datenbank-Adapter für Deepkit ORM. + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/mysql.md b/website/src/translations/de/documentation/package/mysql.md new file mode 100644 index 000000000..a972f372a --- /dev/null +++ b/website/src/translations/de/documentation/package/mysql.md @@ -0,0 +1,17 @@ +# API `@deepkit/mysql` + +```shell +npm install @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('mysql://user:password@localhost/mydatabase'); +// const adapter = new MySQLDatabaseAdapter({host: 'localhost', port: 3306}); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/orm-browser.md b/website/src/translations/de/documentation/package/orm-browser.md new file mode 100644 index 000000000..461e72be5 --- /dev/null +++ b/website/src/translations/de/documentation/package/orm-browser.md @@ -0,0 +1,18 @@ +# Deepkit ORM Browser + +```sh +npm install @deepkit/orm-browser +``` + +Deepkit ORM Browser ist eine Webanwendung, die es ermöglicht, das Database ORM Schema zu durchsuchen, Inhalte zu bearbeiten, Migrationsänderungen einzusehen und die Datenbank zu seeden. + +Dies ist Teil des [Framework-Debuggers](../framework.md), kann aber auch eigenständig verwendet werden. + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/orm.md b/website/src/translations/de/documentation/package/orm.md new file mode 100644 index 000000000..8ee0b58af --- /dev/null +++ b/website/src/translations/de/documentation/package/orm.md @@ -0,0 +1,7 @@ +# API `@deepkit/orm` + +```shell +npm install @deepkit/orm +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/postgres.md b/website/src/translations/de/documentation/package/postgres.md new file mode 100644 index 000000000..3f51e1f54 --- /dev/null +++ b/website/src/translations/de/documentation/package/postgres.md @@ -0,0 +1,18 @@ +# API `@deepkit/postgres` + +```shell +npm install @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('postgres://user:password@localhost/mydatabase'); +// const adapter = new PostgresDatabaseAdapter({ host: 'localhost', database: 'postgres', user: 'postgres' }); + +const database = new Database(adapter); +``` + + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/rpc-tcp.md b/website/src/translations/de/documentation/package/rpc-tcp.md new file mode 100644 index 000000000..7a980d719 --- /dev/null +++ b/website/src/translations/de/documentation/package/rpc-tcp.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc-tcp` + +```shell +npm install @deepkit/rpc-tcp +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/rpc.md b/website/src/translations/de/documentation/package/rpc.md new file mode 100644 index 000000000..a583f5fe4 --- /dev/null +++ b/website/src/translations/de/documentation/package/rpc.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc` + +```shell +npm install @deepkit/rpc +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/run.md b/website/src/translations/de/documentation/package/run.md new file mode 100644 index 000000000..42ab6f2bb --- /dev/null +++ b/website/src/translations/de/documentation/package/run.md @@ -0,0 +1,21 @@ +# API `@deepkit/run` + +```sh +npm install @deepkit/run +``` + +Eine einfache Möglichkeit, TypeScript-Code ohne einen Build-Schritt auszuführen. + +Dieses Tool ist primär für den Einsatz in Deepkits eigener Test-Suite gedacht, kann jedoch auch in eigenen Projekten verwendet werden. + +```typescript +import { typeOf } from '@deepkit/type'; + +console.log(typeOf()); +``` + +```sh +node --import @deepkit/run test.ts +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/sql.md b/website/src/translations/de/documentation/package/sql.md new file mode 100644 index 000000000..3207c73ed --- /dev/null +++ b/website/src/translations/de/documentation/package/sql.md @@ -0,0 +1,7 @@ +# API `@deepkit/sql` + +```shell +npm install @deepkit/sql +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/sqlite.md b/website/src/translations/de/documentation/package/sqlite.md new file mode 100644 index 000000000..5486e2c82 --- /dev/null +++ b/website/src/translations/de/documentation/package/sqlite.md @@ -0,0 +1,16 @@ +# API `@deepkit/sqlite` + +```shell +npm install @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { Database } from '@deepkit/orm'; + +const adapter = new SQLiteDatabaseAdapter(':memory'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/stopwatch.md b/website/src/translations/de/documentation/package/stopwatch.md new file mode 100644 index 000000000..d2a69ed13 --- /dev/null +++ b/website/src/translations/de/documentation/package/stopwatch.md @@ -0,0 +1,7 @@ +# API `@deepkit/stopwatch` + +```shell +npm install @deepkit/stopwatch +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/template.md b/website/src/translations/de/documentation/package/template.md new file mode 100644 index 000000000..e6d908c2b --- /dev/null +++ b/website/src/translations/de/documentation/package/template.md @@ -0,0 +1,7 @@ +# API `@deepkit/template` + +```shell +npm install @deepkit/template +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/topsort.md b/website/src/translations/de/documentation/package/topsort.md new file mode 100644 index 000000000..cbeddd4c2 --- /dev/null +++ b/website/src/translations/de/documentation/package/topsort.md @@ -0,0 +1,7 @@ +# API `@deepkit/topsort` + +```shell +npm install @deepkit/topsort +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/type-compiler.md b/website/src/translations/de/documentation/package/type-compiler.md new file mode 100644 index 000000000..4870002ad --- /dev/null +++ b/website/src/translations/de/documentation/package/type-compiler.md @@ -0,0 +1,9 @@ +# API `@deepkit/type-compiler` + +```shell +npm install @deepkit/type-compiler +``` + +TypeScript transformer, um Laufzeit-Typinformationen zur Laufzeit verfügbar zu machen. + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/type.md b/website/src/translations/de/documentation/package/type.md new file mode 100644 index 000000000..195e08585 --- /dev/null +++ b/website/src/translations/de/documentation/package/type.md @@ -0,0 +1,7 @@ +# API `@deepkit/type` + +```shell +npm install @deepkit/type +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/vite.md b/website/src/translations/de/documentation/package/vite.md new file mode 100644 index 000000000..3f1bcdd0a --- /dev/null +++ b/website/src/translations/de/documentation/package/vite.md @@ -0,0 +1,7 @@ +# API `@deepkit/vite` + +```shell +npm install @deepkit/vite +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/package/workflow.md b/website/src/translations/de/documentation/package/workflow.md new file mode 100644 index 000000000..ff27914f7 --- /dev/null +++ b/website/src/translations/de/documentation/package/workflow.md @@ -0,0 +1,7 @@ +# API `@deepkit/workflow` + +```shell +npm install @deepkit/workflow +``` + + \ No newline at end of file diff --git a/website/src/translations/de/documentation/rpc.md b/website/src/translations/de/documentation/rpc.md new file mode 100644 index 000000000..dddeb39af --- /dev/null +++ b/website/src/translations/de/documentation/rpc.md @@ -0,0 +1,64 @@ +# RPC + +RPC, kurz für Remote Procedure Call, ermöglicht es, Funktionen auf einem entfernten Server so aufzurufen, als wären es lokale Funktionen. Anders als bei der HTTP-Client-Server-Kommunikation, die HTTP-Methoden und eine URL für das Mapping verwendet, nutzt RPC den Funktionsnamen für das Mapping. Die zu sendenden Daten werden als normale Funktionsargumente übergeben, und das Ergebnis des Funktionsaufrufs auf dem Server wird an den Client zurückgesendet. + +Der Vorteil von RPC ist, dass die Client-Server-Abstraktion leichtgewichtig ist, da sie nicht mit Headers, URLs, Query Strings oder Ähnlichem arbeitet. Der Nachteil ist, dass Funktionen auf einem Server via RPC nicht einfach von einem Browser aufgerufen werden können und häufig einen spezifischen Client erfordern. + +Ein zentrales Merkmal von RPC ist, dass die Daten zwischen Client und Server automatisch serialisiert und deserialisiert werden. Daher sind üblicherweise typesafe RPC-Clients möglich. Einige RPC-Frameworks zwingen Nutzer dazu, Types (Parameter Types und Return Types) in einem speziellen Format bereitzustellen. Das kann in Form einer DSL wie Protocol Buffers für gRPC und GraphQL oder eines JavaScript Schema Builders geschehen. Zusätzliche Data Validation kann ebenfalls vom RPC-Framework bereitgestellt werden, wird jedoch nicht von allen unterstützt. + +Deepkit RPC extrahiert Types direkt aus dem TypeScript-Code, sodass es nicht notwendig ist, einen Code Generator zu verwenden oder sie manuell zu definieren. Deepkit unterstützt die automatische Serialisierung und Deserialisierung von Parametern und Ergebnissen. Sobald zusätzliche Einschränkungen in Validation definiert sind, werden sie automatisch validiert. Das macht die Kommunikation über RPC äußerst typesafe und effizient. Die Unterstützung für Streaming über `rxjs` in Deepkit RPC macht dieses RPC-Framework zu einem geeigneten Werkzeug für Echtzeitkommunikation. + +Um das Konzept hinter RPC zu veranschaulichen, betrachten Sie den folgenden Code: + +```typescript +//server.ts +class Controller { + hello(title: string): string { + return 'Hello ' + title + } +} +``` + +Eine Method wie hello wird auf dem Server innerhalb einer Class genau wie eine normale Function implementiert und kann von einem Remote-Client aufgerufen werden. + +```typescript +//client.ts +const client = new RpcClient('localhost'); +const controller = client.controller(); + +const result = await controller.hello('World'); // => 'Hello World'; +``` + +Da RPC grundsätzlich auf asynchroner Kommunikation basiert, erfolgt die Kommunikation in der Regel über HTTP, kann aber auch über TCP oder WebSockets stattfinden. Das bedeutet, dass alle Funktionsaufrufe in TypeScript selbst in ein `Promise` umgewandelt werden. Das Ergebnis kann asynchron mit einem entsprechenden `await` empfangen werden. + +## Isomorphic TypeScript + +Wenn ein Projekt TypeScript sowohl auf dem Client (meist Frontend) als auch auf dem Server (Backend) verwendet, spricht man von Isomorphic TypeScript. Ein typesafe RPC-Framework, das auf den Types von TypeScript basiert, ist für ein solches Projekt besonders vorteilhaft, da Types zwischen Client und Server geteilt werden können. + +Um dies zu nutzen, sollten Types, die auf beiden Seiten verwendet werden, in eine eigene Datei oder ein eigenes Package ausgelagert werden. Das Importieren auf der jeweiligen Seite führt sie dann wieder zusammen. + +```typescript +//shared.ts +export class User { + id: number; + username: string; +} + +//server.ts +import { User } from './shared'; + +@rpc.controller('/user') +class UserController { + async getUser(id: number): Promise { + return await datbase.query(User).filter({id}).findOne(); + } +} + +//client.ts +import { UserControllerApi } from './shared'; +import type { UserController } from './server.ts' +const controller = client.controller('/user'); +const user = await controller.getUser(2); // => User +``` + +Abwärtskompatibilität kann auf die gleiche Weise umgesetzt werden wie bei einer normalen lokalen API: Entweder werden neue Parameter als optional markiert oder es wird eine neue Method hinzugefügt. \ No newline at end of file diff --git a/website/src/translations/de/documentation/rpc/dependency-injection.md b/website/src/translations/de/documentation/rpc/dependency-injection.md new file mode 100644 index 000000000..e410def20 --- /dev/null +++ b/website/src/translations/de/documentation/rpc/dependency-injection.md @@ -0,0 +1,55 @@ +# Dependency Injection + +Controller-Klassen werden vom Dependency Injection Container aus `@deepkit/injector` verwaltet. Bei Verwendung des Deepkit Framework haben diese Controller automatisch Zugriff auf die Provider der Module, die den Controller bereitstellen. + +Im Deepkit Framework werden Controller im Dependency Injection Scope `rpc` instanziiert, wodurch alle Controller automatisch auf verschiedene Provider aus diesem Scope zugreifen können. Diese zusätzlichen Provider sind `HttpRequest` (optional), `RpcInjectorContext`, `SessionState`, `RpcKernelConnection` und `ConnectionWriter`. + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { App } from '@deepkit/app'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +new App({ + providers: [{ provide: Database, useValue: new Database }] + controllers: [Controller], +}).run(); +``` + +Wenn jedoch ein `RpcKernel` manuell instanziiert wird, kann auch ein DI Container übergeben werden. Der RPC-Controller wird dann über diesen DI Container instanziiert. Das ist nützlich, wenn Sie `@deepkit/rpc` in einer Umgebung außerhalb des Deepkit Framework verwenden möchten, wie z. B. Express.js. + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { InjectorContext } from '@deepkit/injector'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +const injector = InjectorContext.forProviders([ + Controller, + { provide: Database, useValue: new Database }, +]); +const kernel = new RpcKernel(injector); +kernel.registerController(Controller); +``` + +Siehe [Dependency Injection](../dependency-injection.md), um mehr zu erfahren. \ No newline at end of file diff --git a/website/src/translations/de/documentation/rpc/errors.md b/website/src/translations/de/documentation/rpc/errors.md new file mode 100644 index 000000000..41ba058a0 --- /dev/null +++ b/website/src/translations/de/documentation/rpc/errors.md @@ -0,0 +1,55 @@ +# Fehler + +Ausgelöste Fehler werden automatisch mit allen Informationen, wie der Fehlermeldung und auch dem Stacktrace, an den Client weitergeleitet. + +Wenn die nominale Instanzidentität des Error-Objekts wichtig ist (weil Sie `instanceof` verwenden), muss `@entity.name('@error:unique-name')` verwendet werden, damit die betreffende Error-Klasse zur Laufzeit registriert und wiederverwendet wird. + +```typescript +@entity.name('@error:myError') +class MyError extends Error {} + +//Server +@rpc.controller('/main') +class Controller { + @rpc.action() + saveUser(user: User): void { + throw new MyError('Can not save user'); + } +} + +//Client +//[MyError] stellt sicher, dass die Klasse MyError zur Laufzeit bekannt ist +const controller = client.controller('/main', [MyError]); + +try { + await controller.getUser(2); +} catch (e) { + if (e instanceof MyError) { + //ups, Benutzer konnte nicht gespeichert werden + } else { + //alle anderen Fehler + } +} +``` + +## Error transformieren + +Da ausgelöste Fehler automatisch mit allen Informationen, wie der Fehlermeldung und auch dem Stacktrace, an den Client weitergeleitet werden, könnten dadurch unerwünschterweise sensible Informationen veröffentlicht werden. Um dies zu ändern, kann in der Methode `transformError` der geworfene Error modifiziert werden. + +```typescript +class MyKernelSecurity extends RpcKernelSecurity { + constructor(private logger: Logger) { + super(); + } + + transformError(error: Error) { + // in neuen Error verpacken + this.logger.error('Error in RPC', error); + return new Error('Something went wrong: ' + error.message); + } +} +``` + +Beachten Sie, dass sobald der Error in einen generischen `Error` umgewandelt wurde, der vollständige Stacktrace und die Identität des Errors verloren gehen. Entsprechend können im Client keine `instanceof`-Checks mehr auf dem Error verwendet werden. + +Wenn Deepkit RPC zwischen zwei Microservices verwendet wird und damit Client und Server vollständig unter der Kontrolle des Entwicklers stehen, ist das Transformieren des Errors selten notwendig. Läuft hingegen der Client in einem Browser bei unbekannten Nutzern, sollte in `transformError` genau darauf geachtet werden, welche Informationen offengelegt werden. Im Zweifel sollte jeder Error mit einem generischen `Error` transformiert werden, um sicherzustellen, dass keine internen Details preisgegeben werden. Das Protokollieren des Errors wäre an dieser Stelle eine gute Idee. \ No newline at end of file diff --git a/website/src/translations/de/documentation/rpc/getting-started.md b/website/src/translations/de/documentation/rpc/getting-started.md new file mode 100644 index 000000000..0ca3b3b88 --- /dev/null +++ b/website/src/translations/de/documentation/rpc/getting-started.md @@ -0,0 +1,147 @@ +# Erste Schritte + +Um Deepkit RPC zu verwenden, muss `@deepkit/type` korrekt installiert sein, da es auf Runtime Types basiert. Siehe [Installation von Runtime Types](../runtime-types.md). + +Sobald dies erfolgreich erledigt ist, kann `@deepkit/rpc` oder das Deepkit Framework, das die Bibliothek bereits unter der Haube verwendet, installiert werden. + +```sh +npm install @deepkit/rpc +``` + +Beachte, dass Controller Classes in `@deepkit/rpc` auf TypeScript Decorators basieren und dieses Feature mit experimentalDecorators aktiviert sein muss. + +Das Package `@deepkit/rpc` muss sowohl auf dem Server als auch auf dem Client installiert sein, wenn sie jeweils eine eigene package.json haben. + +Um über TCP mit dem Server zu kommunizieren, muss das Package `@deepkit/rpc-tcp` auf Client und Server installiert sein. + +```sh +npm install @deepkit/rpc-tcp +``` + +Für die WebSocket-Kommunikation wird das Package ebenfalls auf dem Server benötigt. Der Client im Browser verwendet hingegen WebSocket aus dem offiziellen Standard. + +Wenn der Client auch in einer Umgebung eingesetzt werden soll, in der WebSocket nicht verfügbar ist (zum Beispiel NodeJS), wird das Package ws im Client benötigt. + +```sh +npm install ws +``` + +## Verwendung + +Nachfolgend ein vollständig funktionsfähiges Beispiel basierend auf WebSockets und der Low-Level API von @deepkit/rpc. Bei Verwendung des Deepkit Frameworks werden Controller über App-Module bereitgestellt, und ein RpcKernel wird nicht manuell instanziiert. + +_Datei: server.ts_ + +```typescript +import { rpc, RpcKernel } from '@deepkit/rpc'; +import { RpcWebSocketServer } from '@deepkit/rpc-tcp'; + +@rpc.controller('/main') +export class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } +} + +const kernel = new RpcKernel(); +kernel.registerController(Controller); +const server = new RpcWebSocketServer(kernel, 'localhost:8081'); +server.start({ + host: '127.0.0.1', + port: 8081, +}); +console.log('Server started at ws://127.0.0.1:8081'); + +``` + +_Datei: client.ts_ + +```typescript +import { RpcWebSocketClient } from '@deepkit/rpc'; +import type { Controller } from './server'; + +async function main() { + const client = new RpcWebSocketClient('ws://127.0.0.1:8081'); + const controller = client.controller('/main'); + + const result = await controller.hello('World'); + console.log('result', result); + + client.disconnect(); +} + +main().catch(console.error); + +``` + +## Server Controller + +Der Begriff "Procedure" in Remote Procedure Call wird häufig auch als "Action" bezeichnet. Eine Action ist eine Method, die in einer Class definiert und mit dem Decorator `@rpc.action` markiert ist. Die Class selbst wird mit dem Decorator `@rpc.controller` als Controller markiert und mit einem eindeutigen Namen versehen. Dieser Name wird dann im Client referenziert, um den richtigen Controller anzusprechen. Es können nach Bedarf mehrere Controller definiert und registriert werden. + + +```typescript +import { rpc } from '@deepkit/rpc'; + +@rpc.controller('/main'); +class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } + + @rpc.action() + test(): boolean { + return true; + } +} +``` + +Nur Methods, die als `@rpc.action()` markiert sind, können von einem Client aufgerufen werden. + +Types müssen explizit angegeben werden und können nicht per Type Inference ermittelt werden. Das ist wichtig, da der Serializer genau wissen muss, wie die Types aussehen, um sie in Binärdaten (BSON) oder JSON umzuwandeln, die anschließend über das Netzwerk gesendet werden. + +## Client Controller + +Der normale Ablauf in RPC ist, dass der Client Functions auf dem Server ausführen kann. In Deepkit RPC ist es jedoch auch möglich, dass der Server Functions auf dem Client ausführt. Um dies zu ermöglichen, kann der Client ebenfalls einen Controller registrieren. + +TODO + +## Dependency Injection + +Wenn das Deepkit Framework verwendet wird, wird die Class vom Dependency Injection-Container instanziiert und hat damit automatisch Zugriff auf alle anderen Provider in der Applikation. + +Siehe auch [Dependency Injection](dependency-injection.md#). + +## RxJS-Streaming + +TODO + +## Nominal Types + +Wenn der Client Daten von einem Function-Call erhält, wurden diese zuerst auf dem Server serialisiert und dann auf dem Client deserialisiert. Wenn der Return Type der Function Classes enthält, werden diese Classes auf der Client-Seite rekonstruiert, verlieren jedoch ihre nominale Identität und zugehörigen Methods. Um dieses Problem zu beheben, registriere die Classes als nominal Types mit eindeutigen IDs/Namen. Dieser Ansatz sollte auf alle Classes angewendet werden, die innerhalb einer RPC-API verwendet werden. + +Um eine Class zu registrieren, verwende den Decorator `@entity.name('id')`. + +```typescript +import { entity } from '@deepkit/type'; + +@entity.name('user') +class User { + id!: number; + firstName!: string; + lastName!: string; + get fullName() { + return this.firstName + ' ' + this.lastName; + } +} +``` + +Sobald diese Class als Result einer Function verwendet wird, bleibt ihre Identität erhalten. + +```typescript +const controller = client.controller('/main'); + +const user = await controller.getUser(2); +user instanceof User; //true, wenn @entity.name verwendet wird, und false, wenn nicht +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/rpc/security.md b/website/src/translations/de/documentation/rpc/security.md new file mode 100644 index 000000000..f4c836874 --- /dev/null +++ b/website/src/translations/de/documentation/rpc/security.md @@ -0,0 +1,132 @@ +# Sicherheit + +Standardmäßig können alle RPC-Funktionen von jedem Client aufgerufen werden, und die Peer-to-Peer-Kommunikation ist aktiviert. Um präzise zu steuern, welcher Client was darf, können Sie die Klasse `RpcKernelSecurity` überschreiben. + +```typescript +import { RpcKernelSecurity, Session, RpcControllerAccess } from '@deepkit/type'; + +//enthält Standardimplementierungen +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + return true; + } + + async isAllowedToRegisterAsPeer(session: Session, peerId: string): Promise { + return true; + } + + async isAllowedToSendToPeer(session: Session, peerId: string): Promise { + return true; + } + + async authenticate(token: any): Promise { + throw new Error('Authentication not implemented'); + } + + transformError(err: Error) { + return err; + } +} +``` + +Um dies zu verwenden, übergeben Sie den Provider an den `RpcKernel`: + +```typescript +const kernel = new RpcKernel([{provide: RpcKernelSecurity, useClass: MyKernelSecurity, scope: 'rpc'}]); +``` + +Oder überschreiben Sie im Fall einer Deepkit-App die Klasse `RpcKernelSecurity` mit einem Provider in der App: + +```typescript +import { App } from '@deepkit/type'; +import { RpcKernelSecurity } from '@deepkit/rpc'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + controllers: [MyRpcController], + providers: [ + {provide: RpcKernelSecurity, useClass: MyRpcKernelSecurity, scope: 'rpc'} + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Authentifizierung / Session + +Standardmäßig ist das `Session`-Objekt eine anonyme Session, d. h. der Client hat sich nicht authentifiziert. Wenn der Client sich authentifizieren möchte, wird die Methode `authenticate` aufgerufen. Das von der Methode `authenticate` empfangene Token kommt vom Client und kann jeden beliebigen Wert haben. + +Sobald der Client ein Token setzt, wird die Authentifizierung ausgeführt, wenn die erste RPC-Funktion aufgerufen wird oder wenn `client.connect()` manuell aufgerufen wird. + + +```typescript +const client = new RpcWebSocketClient('localhost:8081'); +client.token.set('123456789'); + +const controller = client.controller('/main'); +``` + +In diesem Fall erhält `RpcKernelSecurity.authenticate` das Token `123456789` und kann entsprechend eine andere Session zurückgeben. Die zurückgegebene Session wird dann an alle anderen Methoden wie `hasControllerAccess` weitergegeben. + +```typescript +import { Session, RpcKernelSecurity } from '@deepkit/rpc'; + +class UserSession extends Session { +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.controllerClassType instanceof MySecureController) { + //MySecureController erfordert UserSession + return session instanceof UserSession; + } + return true; + } + + async authenticate(token: any): Promise { + if (token === '123456789') { + //Benutzername kann eine ID oder ein Benutzername sein + return new UserSession('username', token); + } + throw new Error('Authentication failed'); + } +} +``` + +## Controller-Zugriff + +Die Methode `hasControllerAccess` bestimmt, ob ein Client eine bestimmte RPC-Funktion ausführen darf. Diese Methode wird bei jedem Aufruf einer RPC-Funktion ausgeführt. Gibt sie `false` zurück, wird der Zugriff verweigert und beim Client ein Fehler ausgelöst. + +Das `RpcControllerAccess` enthält wertvolle Informationen über die RPC-Funktion: + +```typescript +interface RpcControllerAccess { + controllerName: string; + controllerClassType: ClassType; + actionName: string; + actionGroups: string[]; + actionData: { [name: string]: any }; +} +``` + +Gruppen und zusätzliche Daten können über den Decorator `@rpc.action()` geändert werden: + +```typescript +class Controller { + @rpc.action().group('secret').data('role', 'admin') + saveUser(user: User): void { + } +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.actionGroups.includes('secret')) { + if (session instanceof UserSession) { + //todo: prüfen + return session.username === 'admin'; + } + return false; + } + return true; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/rpc/transport.md b/website/src/translations/de/documentation/rpc/transport.md new file mode 100644 index 000000000..80cd38bb7 --- /dev/null +++ b/website/src/translations/de/documentation/rpc/transport.md @@ -0,0 +1,17 @@ +# Transportprotokoll + +Deepkit RPC unterstützt mehrere Transportprotokolle. WebSockets ist das Protokoll mit der besten Kompatibilität (da Browser es unterstützen), während es alle Features wie Streaming unterstützt. TCP ist in der Regel schneller und eignet sich hervorragend für die Kommunikation zwischen Servern (Microservices) oder Nicht-Browser-Clients. Aber WebSockets funktionieren auch für die Server-zu-Server-Kommunikation gut. + +## HTTP + +Deepkits RPC-HTTP-Protokoll ist eine Variante, die sich im Browser besonders leicht debuggen lässt, da jeder Funktionsaufruf eine HTTP-Anfrage ist, weist jedoch Einschränkungen auf, wie etwa keine Unterstützung für RxJS-Streaming. + +TODO: Noch nicht implementiert. + +## WebSockets + +@deepkit/rpc-tcp `RpcWebSocketServer` und Browser WebSocket oder Node-`ws`-Package. + +## TCP + +@deepkit/rpc-tcp `RpcNetTcpServer` und `RpcNetTcpClientAdapter` \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types.md b/website/src/translations/de/documentation/runtime-types.md new file mode 100644 index 000000000..833ef0cc4 --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types.md @@ -0,0 +1,9 @@ +# Laufzeit-Typen + +Laufzeit-Typinformationen in TypeScript erschließen neue Workflows und Features, die zuvor nicht verfügbar waren oder Workarounds erforderten. Moderne Entwicklungsprozesse stützen sich stark auf die Deklaration von Types und Schemas für Tools wie GraphQL, Validatoren, ORMs und Encoder wie ProtoBuf. Diese Tools können Entwickler dazu zwingen, neue, einsatzspezifische Sprachen zu lernen, etwa da ProtoBuf und GraphQL ihre eigene Deklarationssprache haben oder Validatoren ihre eigenen Schema-APIs oder JSON-Schema verwenden. + +TypeScript ist inzwischen leistungsfähig genug, um komplexe Strukturen zu beschreiben und sogar Deklarationsformate wie GraphQL, ProtoBuf und JSON-Schema vollständig zu ersetzen. Mit einem Laufzeit-Typsystem ist es möglich, die Anwendungsfälle dieser Tools ohne Code-Generatoren oder JavaScript-Laufzeitbibliotheken für Typdeklarationen wie "Zod" abzudecken. Die Deepkit-Bibliothek hat zum Ziel, Laufzeit-Typinformationen bereitzustellen und die Entwicklung effizienter und kompatibler Lösungen zu erleichtern. + +Deepkit basiert auf der Fähigkeit, Typinformationen zur Laufzeit auszulesen und nutzt der Effizienz wegen so viel TypeScript-Typinformationen wie möglich. Das Laufzeit-Typsystem ermöglicht das Auslesen und Berechnen dynamischer Types, etwa von Class Properties, Function Parameters und Return Types. Deepkit klinkt sich in den Kompilierungsprozess von TypeScript ein, um sicherzustellen, dass alle Typinformationen mithilfe eines [benutzerdefinierten Bytecodes und einer virtuellen Maschine](https://github.com/microsoft/TypeScript/issues/47658) in das generierte JavaScript eingebettet werden, sodass Entwickler programmgesteuert auf Typinformationen zugreifen können. + +Mit Deepkit können Entwickler ihre bestehenden TypeScript Types zur Validierung, Serialisierung und mehr zur Laufzeit verwenden, was den Entwicklungsprozess vereinfacht und ihre Arbeit effizienter macht. \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/bytecode.md b/website/src/translations/de/documentation/runtime-types/bytecode.md new file mode 100644 index 000000000..7397ba7a1 --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/bytecode.md @@ -0,0 +1,76 @@ +# Bytecode + +Dieses Kapitel erläutert im Detail, wie Deepkit die Typinformationen in JavaScript kodiert und ausliest. Es erklärt, wie die Typen tatsächlich in Bytecode umgewandelt, in JavaScript emittiert und zur Laufzeit interpretiert werden. + +## Typen-Compiler + +Der Typen-Compiler (in @deepkit/type-compiler) ist dafür verantwortlich, die definierten Typen in den TypeScript-Dateien einzulesen und in einen Bytecode zu kompilieren. Dieser Bytecode enthält alles Nötige, um die Typen zur Laufzeit auszuführen. +Zum Zeitpunkt der Erstellung dieses Textes ist der Typen-Compiler ein sogenannter TypeScript-Transformer. Dieser Transformer ist ein Plugin für den TypeScript-Compiler selbst und konvertiert einen TypeScript-AST (Abstract Syntax Tree) in einen anderen TypeScript-AST. Deepkits Typen-Compiler liest dabei den AST, erzeugt den entsprechenden Bytecode und fügt ihn in den AST ein. + +TypeScript selbst erlaubt es nicht, dieses Plugin aka Transformer über eine tsconfig.json zu konfigurieren. Entweder muss die TypeScript-Compiler-API direkt verwendet werden oder ein Build-System wie Webpack mit `ts-loader`. Um Deepkit-Nutzern diesen umständlichen Weg zu ersparen, installiert sich der Deepkit-Typen-Compiler automatisch in `node_modules/typescript`, sobald `@deepkit/type-compiler` installiert wird. Dadurch ist es für alle Build-Tools, die auf das lokal installierte TypeScript zugreifen (das in `node_modules/typescript`), möglich, den Typen-Compiler automatisch aktiviert zu haben. So funktionieren tsc, Angular, webpack, ts-node und einige andere Tools automatisch mit Deepkits Typen-Compiler. + +Wenn das automatische Ausführen von NPM-Installationsskripten nicht aktiviert ist und somit das lokal installierte TypeScript nicht angepasst wird, muss dieser Prozess bei Bedarf manuell ausgeführt werden. Alternativ kann der Typen-Compiler manuell in einem Build-Tool wie webpack verwendet werden. Siehe den Installationsabschnitt oben. + +## Bytecode-Encoding + +Der Bytecode ist eine Sequenz von Befehlen für eine virtuelle Maschine und wird im JavaScript selbst als Array aus Referenzen und String (dem eigentlichen Bytecode) kodiert. + +```typescript +//TypeScript +type TypeA = string; + +//generiertes JavaScript +const typeA = ['&']; +``` + +Die vorhandenen Befehle selbst sind jeweils ein Byte groß und finden sich in `@deepkit/type-spec` als `ReflectionOp`-Enums. Zum Zeitpunkt der Erstellung dieses Textes umfasst der Befehlssatz über 81 Befehle. + +```typescript +enum ReflectionOp { + never, + any, + unknown, + void, + object, + + string, + number, + + // ...viele weitere +} +``` + +Eine Befehlssequenz wird zur Speicherersparnis als String kodiert. Ein Typ `string[]` wird dabei als Bytecode-Programm `[string, array]` konzeptualisiert, das die Bytes `[5, 37]` hat und mit folgendem Algorithmus kodiert wird: + +```typescript +function encodeOps(ops: ReflectionOp[]): string { + return ops.map(v => String.fromCharCode(v + 33)).join(''); +} +``` + +Dementsprechend wird eine 5 zum Zeichen `&` und eine 37 zum Zeichen `F`. Zusammen ergeben sie `&F` und werden in JavaScript als `['&F']` emittiert. + +```typescript +//TypeScript +export type TypeA = string[]; + +//generiertes JavaScript +export const __ΩtypeA = ['&F']; +``` + +Um Namenskonflikte zu vermeiden, erhält jeder Typ ein Präfix „_Ω“. Für jeden explizit definierten Typ, der exportiert wird oder von einem exportierten Typ verwendet wird, wird im JavaScript ein Bytecode emittiert. Auch Klassen und Functions erhalten ebenfalls direkt eine Bytecode-Property. + +```typescript +//TypeScript +function log(message: string): void {} + +//generiertes JavaScript +function log(message) {} +log.__type = ['message', 'log', 'P&2!$/"']; +``` + +## Virtuelle Maschine + +Eine virtuelle Maschine (in `@deepkit/type` die Class Processor) ist zur Laufzeit für das Dekodieren und Ausführen des kodierten Bytecodes verantwortlich. Sie gibt stets ein Type-Objekt zurück, wie in der [Reflection-API](./reflection.md) beschrieben. + +Weitere Informationen finden sich in [TypeScript Bytecode Interpreter / Runtime Types #47658](https://github.com/microsoft/TypeScript/issues/47658) \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/custom-serializer.md b/website/src/translations/de/documentation/runtime-types/custom-serializer.md new file mode 100644 index 000000000..c474a7624 --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/custom-serializer.md @@ -0,0 +1,99 @@ +# Benutzerdefinierter Serializer + +Standardmäßig enthält `@deepkit/type` einen JSON-Serializer und Typvalidierung für TypeScript-Typen. Sie können dies erweitern und die Serialisierungsfunktionalität hinzufügen oder entfernen oder ändern, wie die Validierung durchgeführt wird, da die Validierung ebenfalls mit dem Serializer verknüpft ist. + +## Neuer Serializer + +Ein Serializer ist einfach eine Instanz der Klasse `Serializer` mit registrierten Serializer-Templates. Serializer-Templates sind kleine Funktionen, die JavaScript-Code für den JIT-Serializer-Prozess erzeugen. Für jeden Typ (String, Number, Boolean usw.) gibt es ein separates Serializer-Template, das dafür verantwortlich ist, Code für die Datenkonvertierung oder Validierung zurückzugeben. Dieser Code muss mit der JavaScript-Engine kompatibel sein, die der Benutzer verwendet. + +Nur während der Ausführung der Compiler-Template-Funktion haben (oder sollten) Sie vollen Zugriff auf den vollständigen Typ. Die Idee ist, dass Sie alle Informationen, die zur Umwandlung eines Typs benötigt werden, direkt in den JavaScript-Code einbetten, was zu hochoptimiertem Code führt (auch JIT-optimierter Code genannt). + +Das folgende Beispiel erstellt einen leeren Serializer. + +```typescript +import { EmptySerializer } from '@deepkit/type'; + +class User { + name: string = ''; + created: Date = new Date; +} + +const mySerializer = new EmptySerializer('mySerializer'); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 0 } +``` + +Wie Sie sehen, wurde nichts konvertiert (`created` ist immer noch eine Zahl, obwohl wir es als `Date` definiert haben). Um dies zu ändern, fügen wir ein Serializer-Template für die Deserialisierung des Typs Date hinzu. + +```typescript +mySerializer.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); +}); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 2021-06-10T19:34:27.301Z } +``` + +Jetzt konvertiert unser Serializer den Wert in ein Date-Objekt. + +Um dasselbe für die Serialisierung zu tun, registrieren wir ein weiteres Serialisierungs-Template. + +```typescript +mySerializer.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); +}); + +const user1 = new User(); +user1.name = 'Peter'; +user1.created = new Date('2021-06-10T19:34:27.301Z'); +console.log(serialize(user1, undefined, mySerializer)); +``` + +```sh +{ name: 'Peter', created: '2021-06-10T19:34:27.301Z' } +``` + +Unser neuer Serializer konvertiert das Datum im Serialisierungsprozess nun korrekt vom Date-Objekt in einen String. + +## Beispiele + +Viele weitere Beispiele finden Sie im Code der in Deepkit Type enthaltenen [JSON-Serializer](https://github.com/deepkit/deepkit-framework/blob/master/packages/type/src/serializer.ts#L1688). + +## Vorhandenen Serializer erweitern + +Wenn Sie einen vorhandenen Serializer erweitern möchten, können Sie dies mittels Klassenvererbung tun. Das funktioniert, weil Serializer so geschrieben sein sollten, dass sie ihre Templates im Konstruktor registrieren. + +```typescript +class MySerializer extends Serializer { + constructor(name: string = 'mySerializer') { + super(name); + this.registerTemplates(); + } + + protected registerTemplates() { + this.deserializeRegistry.register(ReflectionKind.string, (type, state) => { + state.addSetter(`String(${state.accessor})`); + }); + + this.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); + }); + + this.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); + }); + } +} +const mySerializer = new MySerializer(); +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/extend.md b/website/src/translations/de/documentation/runtime-types/extend.md new file mode 100644 index 000000000..cd70b10fc --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/extend.md @@ -0,0 +1,56 @@ +# Erweitern + +## Benutzerdefinierte Serialisierung + +Sie können die Serialisierung für einen Type erweitern, indem Sie entweder Ihren eigenen `Serializer` schreiben oder den Standard-`serializer` erweitern. + +Dieses Beispiel zeigt, wie man eine Class `Point` in ein Tuple `[number, number]` serialisiert und deserialisiert. + +```typescript +import { serializer, SerializationError } from '@deepkit/type'; + +class Point { + constructor(public x: number, public y: number) { + } +} + +// Deserialisieren bedeutet von JSON zu einer (Class-)Instanz. +serializer.deserializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: any) => { + // wenn es bereits eine Point-Instanz ist, sind wir fertig + if (v instanceof Point) return v; + + // an diesem Punkt könnte `v` alles Mögliche sein (außer undefined), daher müssen wir prüfen + if (!Array.isArray(v)) throw new SerializationError('Expected array'); + if (v.length !== 2) throw new SerializationError('Expected array with two elements'); + if (typeof v[0] !== 'number' || typeof v[1] !== 'number') throw new SerializationError('Expected array with two numbers'); + return new Point(v[0], v[1]); + }); +}); + +serializer.serializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: Point) => { + // an diesem Punkt ist `v` immer eine Point-Instanz + return [v.x, v.y]; + }); +}); + +// cast und deserialize verwenden standardmäßig den `serializer` +const point = cast([1, 2], undefined, serializer); +expect(point).toBeInstanceOf(Point); +expect(point.x).toBe(1); +expect(point.y).toBe(2); + +{ + expect(() => deserialize(['vbb'])).toThrowError(SerializationError); + expect(() => deserialize(['vbb'])).toThrow('Expected array with two elements') +} + +// serialize verwendet standardmäßig den `serializer` +const json = serialize(point); +expect(json).toEqual([1, 2]); +``` + +Bitte beachten Sie, dass dies nur für die regulären `@deepkit/type` Functions wie `cast`, `deserialize` und `serialize` funktioniert. + +Dies wird nicht in die Datenbankschicht übernommen, da die Datenbankschicht die Types verwendet, wie sie in der Entity Class für Migration und Serialisierung definiert sind (z. B. BSON-Serialisierung). \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/external-types.md b/website/src/translations/de/documentation/runtime-types/external-types.md new file mode 100644 index 000000000..7e0244b3a --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/external-types.md @@ -0,0 +1,49 @@ +# Externe Types + +## Externe Classes + +Da TypeScript standardmäßig keine Type-Informationen enthält, haben importierte Types/Classes aus anderen Paketen (die @deepkit/type-compiler nicht verwendet haben) keine Type-Informationen verfügbar. + +Um Types für eine externe Class zu annotieren, verwende `annotateClass` und stelle sicher, dass diese Function in der Bootstrap-Phase deiner Anwendung ausgeführt wird, bevor die importierte Class anderswo verwendet wird. + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +interface AnnotatedClass { + id: number; + title: string; +} + +annotateClass(MyExternalClass); + +//alle Verwendungen von MyExternalClass liefern nun den Type von AnnotatedClass zurück +serialize({...}); + +//MyExternalClass kann nun auch in anderen Types verwendet werden +interface User { + id: number; + clazz: MyExternalClass; +} +``` + +`MyExternalClass` kann nun in Serialisierungsfunktionen und in der Reflection-API verwendet werden. + +Das Folgende zeigt, wie man generische Classes annotiert: + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +class AnnotatedClass { + id!: T; +} + +annotateClass(ExternalClass, AnnotatedClass); +``` + +## Import Type + +Die Syntax `import type` wurde von TypeScript entwickelt, um zu vermeiden, dass tatsächlicher JavaScript-Code importiert wird, und ihn nur für Type-Checking zu verwenden. Das ist z. B. dann nützlich, wenn du einen Type aus einem Package verwenden möchtest, der zur Runtime nicht verfügbar ist, sondern nur zur Compile-Time, oder wenn du dieses Package zur Runtime nicht wirklich laden möchtest. + +Deepkit unterstützt das Konzept von `import type` und generiert keinen Runtime-Code. Das bedeutet, wenn du `import type` verwendest, sind zur Runtime keine Type-Informationen verfügbar. \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/getting-started.md b/website/src/translations/de/documentation/runtime-types/getting-started.md new file mode 100644 index 000000000..7d7c3bb57 --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/getting-started.md @@ -0,0 +1,106 @@ +# Erste Schritte + +Um Deepkits Runtime Type System zu installieren, werden zwei Packages benötigt: der Deepkit Type Compiler und das Deepkit Type Package selbst. Der Type Compiler ist ein TypeScript Transformer, der Runtime Type Information aus TypeScript Types generiert. Das Type Package enthält die Runtime Virtual Machine und Type Annotations sowie viele nützliche Funktionen für die Arbeit mit Types. + + +## Installation + +```sh +npm install --save @deepkit/type +npm install --save-dev @deepkit/type-compiler typescript ts-node +``` + +Runtime Type Information wird nicht standardmäßig generiert. Dazu muss `"reflection": true` in der Datei `tsconfig.json` gesetzt werden, um sie zu aktivieren. + +Wenn Decorators verwendet werden sollen, muss `"experimentalDecorators": true` in `tsconfig.json` aktiviert sein. Das ist nicht zwingend erforderlich, um mit `@deepkit/type` zu arbeiten, aber für bestimmte Funktionen anderer Deepkit Libraries und im Deepkit Framework notwendig. + +_Datei: tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +Schreiben Sie Ihren ersten Code mit Runtime Type Information: + +_Datei: app.ts_ + +```typescript +import { cast, MinLength, ReflectionClass } from '@deepkit/type'; + +interface User { + username: string & MinLength<3>; + birthDate?: Date; +} + +const user = cast({ + username: 'Peter', + birthDate: '2010-10-10T00:00:00Z' +}); +console.log(user); + +const reflection = ReflectionClass.from(); +console.log(reflection.getProperty('username').type); +``` + +Und führen Sie es mit `ts-node` aus: + +```sh +./node_modules/.bin/ts-node app.ts +``` + +## Interaktives Beispiel + +Hier ist ein CodeSandbox-Beispiel: https://codesandbox.io/p/sandbox/deepkit-runtime-types-fjmc2f?file=index.ts + +## Type Compiler + +TypeScript selbst erlaubt es nicht, den Type Compiler über eine `tsconfig.json` zu konfigurieren. Dazu muss entweder die TypeScript Compiler API direkt verwendet werden oder ein Build-System wie Webpack mit _ts-loader_. Um Deepkit-Nutzern diesen umständlichen Weg zu ersparen, installiert sich der Deepkit Type Compiler automatisch in `node_modules/typescript`, sobald `@deepkit/type-compiler` installiert ist (dies geschieht über NPM Install Hooks). +Dadurch haben alle Build-Tools, die auf das lokal installierte TypeScript (das in `node_modules/typescript`) zugreifen, den Type Compiler automatisch aktiviert. So funktionieren _tsc_, Angular, webpack, _ts-node_ und einige andere Tools automatisch mit dem Deepkit Type Compiler. + +Falls der Type Compiler nicht automatisch erfolgreich installiert werden konnte (z. B. weil NPM Install Hooks deaktiviert sind), kann dies manuell mit folgendem Befehl erfolgen: + +```sh +node_modules/.bin/deepkit-type-install +``` + +Beachten Sie, dass `deepkit-type-install` ausgeführt werden muss, wenn die lokale typescript-Version aktualisiert wurde (z. B. wenn sich die typescript-Version in package.json geändert hat und `npm install` ausgeführt wird). + +## Webpack + +Wenn Sie den Type Compiler in einem webpack-Build verwenden möchten, können Sie dies mit dem `ts-loader` Package tun (oder mit einem anderen TypeScript Loader, der Transformer Registration unterstützt). + +_Datei: webpack.config.js_ + +```javascript +const typeCompiler = require('@deepkit/type-compiler'); + +module.exports = { + entry: './app.ts', + module: { + rules: [ + { + test: /\.tsx?$/, + use: { + loader: 'ts-loader', + options: { + //aktiviert den Type Compiler von @deepkit/type + getCustomTransformers: (program, getProgram) => ({ + before: [typeCompiler.transformer], + afterDeclarations: [typeCompiler.declarationTransformer], + }), + } + }, + exclude: /node_modules/, + }, + ], + }, +} +``` \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/reflection.md b/website/src/translations/de/documentation/runtime-types/reflection.md new file mode 100644 index 000000000..1fbe977f8 --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/reflection.md @@ -0,0 +1,291 @@ +# Reflection + +Um direkt mit den Typinformationen selbst zu arbeiten, gibt es zwei grundlegende Varianten: Type-Objekte und Reflection-Klassen. Type-Objekte sind reguläre JS-Objekte, die von `typeOf()` zurückgegeben werden. Reflection-Klassen werden unten besprochen. + + +Die Function `typeOf` funktioniert für alle Types, einschließlich Interfaces, object literals, Classes, Functions und Type Aliases. Sie gibt ein Type-Objekt zurück, das alle Informationen über den Type enthält. Du kannst jeden Type als Type Argument übergeben, einschließlich Generics. + +```typescript +import { typeOf } from '@deepkit/type'; + +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} + +typeOf<{id: number}>(); //{kind: 4, types: [{kind: 6, name: 'id'}]} + +class User { + id: number +} + +typeOf(); //{kind: 4, types: [...]} + +function test(id: number): string {} + +typeOf(); //{kind: 12, parameters: [...], return: {kind: 5}} +``` + +Das Type-Objekt ist ein einfaches object literal, das eine Property `kind` enthält, die den Typ des Type-Objekts angibt. Die Property `kind` ist eine Zahl und erhält ihre Bedeutung aus dem Enum `ReflectionKind`. `ReflectionKind` ist im Package `@deepkit/type` wie folgt definiert: + +```typescript +enum ReflectionKind { + never, //0 + any, //1 + unknown, //2 + void, //3 + object, //4 + string, //5 + number, //6 + boolean, //7 + symbol, //8 + bigint, //9 + null, //10 + undefined, //11 + + //... und noch mehr +} +``` + +Es gibt eine Reihe möglicher Type-Objekte, die zurückgegeben werden können. Die einfachsten sind `never`, `any`, `unknown`, `void, null,` und `undefined`, die wie folgt dargestellt werden: + +```typescript +{kind: 0}; //never +{kind: 1}; //any +{kind: 2}; //unknown +{kind: 3}; //void +{kind: 10}; //null +{kind: 11}; //undefined +``` + +Beispielsweise ist die Zahl 0 der erste Eintrag des `ReflectionKind`-Enums, in diesem Fall `never`, die Zahl 1 ist der zweite Eintrag, hier `any`, und so weiter. Dementsprechend werden primitive Types wie `string`, `number`, `boolean` wie folgt dargestellt: + +```typescript +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} +typeOf(); //{kind: 7} +``` + +Diese eher einfachen Types haben keine weiteren Informationen am Type-Objekt, da sie direkt als Type Argument an `typeOf` übergeben wurden. Wenn Types jedoch über Type Aliases übergeben werden, sind zusätzliche Informationen am Type-Objekt zu finden. + +```typescript +type Title = string; + +typeOf(); //{kind: 5, typeName: 'Title'} +``` + +In diesem Fall ist auch der Name des Type Alias 'Title' verfügbar. Wenn ein Type Alias ein Generic ist, sind die übergebenen Types ebenfalls am Type-Objekt verfügbar. + +```typescript +type Title<T> = T extends true ? string : number; + +typeOf<Title<true>>(); +{kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]} +``` + +Wenn der übergebene Type das Ergebnis eines Index Access Operators ist, sind der Container und der Index Type vorhanden: + +```typescript +interface User { + id: number; + username: string; +} + +typeOf<User['username']>(); +{kind: 5, indexAccessOrigin: { + container: {kind: Reflection.objectLiteral, types: [...]}, + Index: {kind: Reflection.literal, literal: 'username'} +}} +``` + +Interfaces und object literals werden beide als Reflection.objectLiteral ausgegeben und enthalten die Properties und Methods im Array `types`. + +```typescript +interface User { + id: number; + username: string; + login(password: string): void; +} + +typeOf<User>(); +{ + kind: Reflection.objectLiteral, + types: [ + {kind: Reflection.propertySignature, name: 'id', type: {kind: 6}}, + {kind: Reflection.propertySignature, name: 'username', + type: {kind: 5}}, + {kind: Reflection.methodSignature, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} + +type User = { + id: number; + username: string; + login(password: string): void; +} +typeOf<User>(); //gibt dasselbe Objekt wie oben zurück +``` + +Index Signatures befinden sich ebenfalls im Array `types`. + +```typescript +interface BagOfNumbers { + [name: string]: number; +} + + +typeOf<BagOfNumbers>; +{ + kind: Reflection.objectLiteral, + types: [ + { + kind: Reflection.indexSignature, + index: {kind: 5}, //string + type: {kind: 6}, //number + } + ] +} + +type BagOfNumbers = { + [name: string]: number; +} +typeOf<BagOfNumbers>(); //gibt dasselbe Objekt wie oben zurück +``` + +Classes sind ähnlich wie object literals und haben zusätzlich zu `classType`, welches eine Referenz auf die Class selbst ist, ihre Properties und Methods unter einem `types`-Array. + +```typescript +class User { + id: number = 0; + username: string = ''; + login(password: string): void { + //nichts tun + } +} + +typeOf<User>(); +{ + kind: Reflection.class, + classType: User, + types: [ + {kind: Reflection.property, name: 'id', type: {kind: 6}}, + {kind: Reflection.property, name: 'username', + type: {kind: 5}}, + {kind: Reflection.method, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} +``` + +Beachte, dass sich der Type von Reflection.propertySignature zu Reflection.property und Reflection.methodSignature zu Reflection.method geändert hat. Da Properties und Methods auf Classes zusätzliche Attribute haben, können diese Informationen ebenfalls abgefragt werden. Letztere enthalten zusätzlich `visibility`, `abstract` und `default`. +Type-Objekte von Classes enthalten nur die Properties und Methods der Class selbst und nicht der Super-Classes. Dies steht im Gegensatz zu Type-Objekten von Interfaces/object literals, die alle Property Signatures und Method Signatures aller Parents im `types`-Array aufgelöst haben. Um die Properties und Methods der Super-Classes aufzulösen, können entweder ReflectionClass und dessen `ReflectionClass.getProperties()` (siehe folgende Abschnitte) oder `resolveTypeMembers()` aus `@deepkit/type` verwendet werden. + +Es gibt eine ganze Fülle an Type-Objekten. Zum Beispiel für literal, template literals, promise, enum, union, array, tuple und viele mehr. Um herauszufinden, welche es alle gibt und welche Informationen verfügbar sind, wird empfohlen, `Type` aus `@deepkit/type` zu importieren. Es ist ein `union` mit allen möglichen Subtypes wie TypeAny, TypeUnknonwn, TypeVoid, TypeString, TypeNumber, TypeObjectLiteral, TypeArray, TypeClass und vielen mehr. Dort findest du die genaue Struktur. + +## Type Cache + +Type-Objekte werden für Type Aliases, Functions und Classes gecacht, sobald kein Generic-Argument übergeben wird. Das bedeutet, ein Aufruf von `typeOf<MyClass>()` gibt immer dasselbe Objekt zurück. + +```typescript +type MyType = string; + +typeOf<MyType>() === typeOf<MyType>(); //true +``` + +Sobald jedoch ein Generic Type verwendet wird, werden immer neue Objekte erstellt, selbst wenn der übergebene Type immer derselbe ist. Dies liegt daran, dass theoretisch eine unendliche Anzahl von Kombinationen möglich ist und ein solcher Cache effektiv ein Memory Leak wäre. + +```typescript +type MyType<T> = T; + +typeOf<MyType<string>>() === typeOf<MyType<string>>(); +//false +``` + +Sobald ein Type jedoch in einem rekursiven Type mehrfach instanziiert wird, wird er gecacht. Die Dauer des Caches ist jedoch nur auf den Moment begrenzt, in dem der Type berechnet wird, und existiert danach nicht mehr. Außerdem wird zwar das Type-Objekt gecacht, es wird aber eine neue Referenz zurückgegeben und ist nicht exakt dasselbe Objekt. + +```typescript +type MyType<T> = T; +type Object = { + a: MyType<string>; + b: MyType<string>; +}; + +typeOf<Object>(); +``` + +`MyType<string>` wird so lange gecacht, wie `Object` berechnet wird. Die PropertySignature von `a` und `b` haben somit denselben `type` aus dem Cache, sind aber nicht dasselbe Type-Objekt. + +Alle nicht-root Type-Objekte haben eine Property `parent`, die üblicherweise auf den umschließenden Parent zeigt. Dies ist zum Beispiel wertvoll, um herauszufinden, ob ein Type Teil einer Union ist oder nicht. + +```typescript +type ID = string | number; + +typeOf<ID>(); +*Ref 1* { + kind: ReflectionKind.union, + types: [ + {kind: ReflectionKind.string, parent: *Ref 1* } } + {kind: ReflectionKind.number, parent: *Ref 1* } + ] +} +``` + +‚Ref 1‘ zeigt auf das eigentliche Union Type-Objekt. + +Bei gecachten Type-Objekten wie oben exemplarisch dargestellt, sind die `parent`-Properties nicht immer die echten Parents. Beispielsweise bei einer Class, die mehrfach verwendet wird: Obwohl unmittelbare Types in `types` (TypePropertySignature und TypeMethodSignature) auf die korrekte TypeClass zeigen, zeigen die `type`-Properties dieser Signature Types auf die Signature Types der TypeClass des gecachten Eintrags. Das ist wichtig zu wissen, um nicht die Parent-Struktur unendlich zu traversieren, sondern nur den unmittelbaren Parent. Die Tatsache, dass der Parent keine unendliche Präzision hat, ist aus Performance-Gründen so. + +## JIT Cache + +Im weiteren Verlauf werden einige Functions und Features beschrieben, die oft auf den Type-Objekten basieren. Um einige davon performant zu implementieren, wird ein JIT (just in time) Cache pro Type-Objekt benötigt. Dieser kann über `getJitContainer(type)` bereitgestellt werden. Diese Function gibt ein einfaches Objekt zurück, auf dem beliebige Daten gespeichert werden können. Solange keine Referenz auf das Objekt gehalten wird, wird es automatisch vom GC gelöscht, sobald das Type-Objekt selbst ebenfalls nicht mehr referenziert wird. + + +## Reflection-Klassen + +Zusätzlich zur Function `typeOf<>()` gibt es verschiedene Reflection-Klassen, die eine OOP-Alternative zu den Type-Objekten bieten. Die Reflection-Klassen sind nur für Classes, Interface/object literals und Functions sowie deren direkte Sub-Types (Properties, Methods, Parameters) verfügbar. Alle tieferen Types müssen wieder mit den Type-Objekten gelesen werden. + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + + +const reflection = ReflectionClass.from<User>(); + +reflection.getProperties(); //[ReflectionProperty, ReflectionProperty] +reflection.getProperty('id'); //ReflectionProperty + +reflection.getProperty('id').name; //'id' +reflection.getProperty('id').type; //{kind: ReflectionKind.number} +reflection.getProperty('id').isOptional(); //false +``` + + +## Typinformationen empfangen + +Um Functions bereitzustellen, die auf Types operieren, kann es nützlich sein, dem Benutzer anzubieten, einen Type manuell zu übergeben. Zum Beispiel könnte es in einer Validierungsfunction sinnvoll sein, den anzufordernden Type als erstes Type Argument und die zu validierenden Daten als erstes Function Argument bereitzustellen. + +```typescript +validate<string>(1234); +``` + +Damit diese Function den Type `string` erhält, muss sie dies dem Type Compiler mitteilen. + +```typescript +function validate<T>(data: any, type?: ReceiveType<T>): void; +``` + +`ReceiveType` mit dem Verweis auf das erste Type Argument `T` signalisiert dem Type Compiler, dass jeder Aufruf von `validate` den Type an zweiter Stelle ablegen soll (da `type` an zweiter Stelle deklariert ist). Um die Informationen zur Laufzeit auszulesen, wird anschließend die Function `resolveReceiveType` verwendet. + +```typescript +import { resolveReceiveType, ReceiveType } from '@deepkit/type'; + +function validate<T>(data: any, type?: ReceiveType<T>): void { + type = resolveReceiveType(type); +} +``` + +Es ist sinnvoll, das Ergebnis derselben Variable zuzuweisen, um nicht unnötig eine neue zu erstellen. In `type` ist nun entweder ein Type-Objekt gespeichert oder es wird ein Error geworfen, wenn z. B. kein Type Argument übergeben wurde, Deepkits Type Compiler nicht korrekt installiert wurde oder das Emittieren von Typinformationen nicht aktiviert ist (siehe den Abschnitt Installation oben). \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/serialization.md b/website/src/translations/de/documentation/runtime-types/serialization.md new file mode 100644 index 000000000..099a4e053 --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/serialization.md @@ -0,0 +1,153 @@ +# Serialisierung + +Serialisierung ist der Prozess, Datentypen in ein Format zu überführen, das z. B. für Transport oder Speicherung geeignet ist. Deserialisierung ist der Prozess, dies rückgängig zu machen. Dies geschieht verlustfrei, das bedeutet, dass Daten zu und von einem Serialisierungsziel konvertiert werden können, ohne Typinformationen oder die Daten selbst zu verlieren. + +In JavaScript erfolgt Serialisierung üblicherweise zwischen JavaScript-Objekten und JSON. JSON unterstützt nur String, Number, Boolean, Objects und Arrays. JavaScript hingegen unterstützt viele weitere Typen wie BigInt, ArrayBuffer, typed arrays, Date, benutzerdefinierte Class-Instanzen und viele mehr. Um JavaScript-Daten per JSON an einen Server zu übertragen, benötigen Sie also einen Serialisierungsprozess (auf dem Client) und einen Deserialisierungsprozess (auf dem Server), oder umgekehrt, wenn der Server Daten als JSON an den Client sendet. `JSON.parse` und `JSON.stringify` zu verwenden, reicht dafür oft nicht aus, da dies nicht verlustfrei ist. + +Dieser Serialisierungsprozess ist für nicht-triviale Daten unbedingt erforderlich, da JSON seine Informationen selbst bei grundlegenden Typen wie einem Datum verliert. Ein neues Date wird letztlich als String in JSON serialisiert: + +```typescript +const json = JSON.stringify(new Date); +//'"2022-05-13T20:48:51.025Z" +``` + +Wie Sie sehen, ist das Ergebnis von JSON.stringify ein JSON-String. Wenn Sie es erneut mit JSON.parse deserialisieren, erhalten Sie kein Date-Objekt, sondern einen String. + +```typescript +const value = JSON.parse('"2022-05-13T20:48:51.025Z"'); +//"2022-05-13T20:48:51.025Z" +``` + +Obwohl es verschiedene Workarounds gibt, um JSON.parse beizubringen, Date-Objekte zu deserialisieren, sind sie fehleranfällig und leistungsschwach. Um typsichere Serialisierung und Deserialisierung für diesen Fall und viele andere Types zu ermöglichen, ist ein Serialisierungsprozess notwendig. + +Es stehen vier Hauptfunktionen zur Verfügung: `serialize`, `cast`, `deserialize` und `validatedDeserialize`. Unter der Haube dieser Funktionen wird standardmäßig der global verfügbare JSON-Serializer aus `@deepkit/type` verwendet, es kann aber auch ein benutzerdefiniertes Serialisierungsziel verwendet werden. + +Deepkit Type unterstützt benutzerdefinierte Serialisierungsziele, bringt jedoch bereits ein leistungsstarkes JSON-Serialisierungsziel mit, das Daten als JSON-Objekte serialisiert und dann mittels JSON.stringify korrekt und sicher in JSON umgewandelt werden kann. Mit `@deepkit/bson` kann auch BSON als Serialisierungsziel verwendet werden. Wie man ein benutzerdefiniertes Serialisierungsziel erstellt (z. B. für einen Datenbanktreiber), erfahren Sie im Abschnitt Custom Serializer. + +Beachten Sie, dass Serializer zwar auch Daten auf Kompatibilität validieren, diese Validierungen jedoch von der Validierung in [Validierung](validation.md) abweichen. Nur die Funktion `cast` ruft nach erfolgreicher Deserialisierung auch den vollständigen Validierungsprozess aus dem Kapitel [Validierung](validation.md) auf und wirft einen Fehler, wenn die Daten nicht gültig sind. + +Alternativ kann `validatedDeserialize` verwendet werden, um nach der Deserialisierung zu validieren. Eine weitere Alternative ist, die Funktionen `validate` oder `validates` manuell auf deserialisierte Daten aus der Funktion `deserialize` aufzurufen, siehe [Validierung](validation.md). + +Alle Funktionen aus Serialisierung und Validierung werfen bei Fehlern einen `ValidationError` aus `@deepkit/type`. + +## Cast + +Die Funktion `cast` erwartet als erstes Type-Argument einen TypeScript Type und als zweites Argument die zu konvertierenden Daten. Die Daten werden in den angegebenen Type gecastet und bei Erfolg zurückgegeben. Wenn die Daten mit dem angegebenen Type nicht kompatibel sind und nicht automatisch konvertiert werden können, wird ein `ValidationError` geworfen. + +```typescript +import { cast } from '@deepkit/type'; + +cast<string>(123); //'123' +cast<number>('123'); //123 +cast<number>('asdasd'); // wirft ValidationError + +cast<string | number>(123); //123 +``` + +```typescript +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = cast<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); +``` + +Die Funktion `deserialize` ist `cast` ähnlich, wirft jedoch keinen Fehler, wenn die Daten mit dem angegebenen Type nicht kompatibel sind. Stattdessen werden die Daten so weit wie möglich konvertiert und das Ergebnis zurückgegeben. Wenn die Daten nicht kompatibel sind, werden sie unverändert zurückgegeben. + +## Serialisierung + +```typescript +import { serialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const model = new MyModel('Peter'); + +const jsonObject = serialize<MyModel>(model); +//{ +// id: 0, +// created: '2021-06-10T15:07:24.292Z', +// name: 'Peter' +//} +const json = JSON.stringify(jsonObject); +``` + +Die Funktion `serialize` konvertiert die übergebenen Daten standardmäßig mit dem JSON-Serializer in ein JSON-Objekt, also: String, Number, Boolean, Object oder Array. Das Ergebnis kann anschließend sicher mit `JSON.stringify` zu JSON konvertiert werden. + +## Deserialisierung + +Die Funktion `deserialize` konvertiert die übergebenen Daten standardmäßig mit dem JSON-Serializer in die entsprechenden angegebenen Types. Der JSON-Serializer erwartet ein JSON-Objekt, d. h.: string, number, boolean, object oder array. Dieses erhält man üblicherweise aus einem `JSON.parse`-Aufruf. + +```typescript +import { deserialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = deserialize<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); + +//aus JSON +const json = '{"id": 5, "created": "Sat Oct 13 2018 14:17:35 GMT+0200", "name": "Peter"}'; +const myModel = deserialize<MyModel>(JSON.parse(json)); +``` + +Wenn bereits der korrekte Datentyp übergeben wird (z. B. ein Date-Objekt im Fall von `created`), dann wird dieses unverändert übernommen. + +Es kann nicht nur eine Class, sondern jeder TypeScript Type als erstes Type-Argument angegeben werden. Somit können auch Primitive Types oder sehr komplexe Types übergeben werden: + +```typescript +deserialize<Date>('Sat Oct 13 2018 14:17:35 GMT+0200'); +deserialize<string | number>(23); +``` + +<a name="loosely-convertion"></a> +### Lockere Typkonvertierung + +Im Deserialisierungsprozess ist eine lockere Typkonvertierung implementiert. Das bedeutet, dass String und Number für entsprechende Types akzeptiert und automatisch konvertiert werden können, z. B. eine Number für einen String Type oder umgekehrt. Das ist z. B. nützlich, wenn Daten über eine URL angenommen und an den Deserializer übergeben werden. Da die URL immer ein String ist, versucht Deepkit Type dennoch, die Types für Number und Boolean aufzulösen. + +```typescript +deserialize<boolean>('false')); //false +deserialize<boolean>('0')); //false +deserialize<boolean>('1')); //true + +deserialize<number>('1')); //1 + +deserialize<string>(1)); //'1' +``` + +Die folgenden lockeren Typkonvertierungen sind im JSON-Serializer eingebaut: + +* number|bigint: Number oder BigInt akzeptieren String, Number und BigInt. `parseFloat` bzw. `BigInt(x)` werden bei einer notwendigen Konvertierung verwendet. +* boolean: Boolean akzeptiert Number und String. 0, '0', 'false' wird als `false` interpretiert. 1, '1', 'true' wird als `true` interpretiert. +* string: String akzeptiert Number, String, Boolean und vieles mehr. Alle Nicht-String-Werte werden automatisch mit `String(x)` konvertiert. + +Die lockere Konvertierung kann auch deaktiviert werden: + +```typescript +const result = deserialize(data, {loosely: false}); +``` + +Bei ungültigen Daten wird nicht versucht, diese zu konvertieren, stattdessen wird ein Fehler ausgelöst. \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/types.md b/website/src/translations/de/documentation/runtime-types/types.md new file mode 100644 index 000000000..52d56957b --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/types.md @@ -0,0 +1,494 @@ +# Typannotationen + +Typannotationen sind normale TypeScript-Types, die Metainformationen enthalten, die zur Laufzeit ausgelesen werden können und das Verhalten verschiedener Functions zur Laufzeit ändern. Deepkit stellt bereits einige Typannotationen bereit, die viele Anwendungsfälle abdecken. Zum Beispiel kann eine Klassen-Property als Primary Key, Reference oder Index markiert werden. Die Datenbankbibliothek kann diese Informationen zur Laufzeit nutzen, um ohne vorherige Code-Generierung die richtigen SQL-Abfragen zu erstellen. + +Validator-Constraints wie `MaxLength`, `Maximum` oder `Positive` können ebenfalls zu jedem Type hinzugefügt werden. Es ist außerdem möglich, dem Serializer mitzuteilen, wie ein bestimmter Wert serialisiert oder deserialisiert werden soll. Darüber hinaus ist es möglich, vollständig eigene Typannotationen zu erstellen und diese zur Laufzeit auszulesen, um das Type-System zur Laufzeit sehr individuell zu nutzen. + +Deepkit bringt einen ganzen Satz an Typannotationen mit, die alle direkt aus `@deepkit/type` genutzt werden können. Sie sind so konzipiert, dass sie nicht aus mehreren Libraries stammen, um Code nicht direkt an eine bestimmte Library wie Deepkit RPC oder Deepkit Database zu binden. Dadurch lassen sich Types leichter wiederverwenden, sogar im Frontend, auch wenn zum Beispiel Datenbank-Typannotationen verwendet werden. + +Im Folgenden befindet sich eine Liste vorhandener Typannotationen. Der Validator und Serializer von `@deepkit/type` und `@deepkit/bson` sowie die Deepkit Database von `@deepkit/orm` nutzen diese Informationen jeweils unterschiedlich. Siehe die entsprechenden Kapitel, um mehr darüber zu erfahren. + +## Integer/Float + +Integer und Floats sind als Basis als `number` definiert und haben mehrere Untervarianten: + +| Typ | Beschreibung | +|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| integer | Ein Integer beliebiger Größe. | +| int8 | Ein Integer zwischen -128 und 127. | +| uint8 | Ein Integer zwischen 0 und 255. | +| int16 | Ein Integer zwischen -32768 und 32767. | +| uint16 | Ein Integer zwischen 0 und 65535. | +| int32 | Ein Integer zwischen -2147483648 und 2147483647. | +| uint32 | Ein Integer zwischen 0 und 4294967295. | +| float | Entspricht number, kann im Datenbankkontext jedoch eine andere Bedeutung haben. | +| float32 | Ein Float zwischen -3.40282347e+38 und 3.40282347e+38. Beachte, dass JavaScript den Bereich aufgrund von Präzisionsproblemen nicht korrekt prüfen kann; die Information kann jedoch für die Datenbank oder binäre Serializer nützlich sein. | +| float64 | Entspricht number, kann im Datenbankkontext jedoch eine andere Bedeutung haben. | + +```typescript +import { integer } from '@deepkit/type'; + +interface User { + id: integer; +} +``` + +Hier ist die `id` des Users zur Laufzeit eine number, wird aber in Validierung und Serialisierung als Integer interpretiert. +Das bedeutet, dass beispielsweise in der Validierung keine Floats erlaubt sind und der Serializer Floats automatisch in Integer umwandelt. + +```typescript +import { is, integer } from '@deepkit/type'; + +is<integer>(12); //true +is<integer>(12.5); //false +``` + +Die Subtypes können auf die gleiche Weise verwendet werden und sind nützlich, wenn ein bestimmter Zahlenbereich erlaubt werden soll. + +```typescript +import { is, int8 } from '@deepkit/type'; + +is<int8>(-5); //true +is<int8>(5); //true +is<int8>(-200); //false +is<int8>(2500); //false +``` + +```typescript +import { is, float, float32, float64 } from '@deepkit/type'; +is<float>(12.5); //true +is<float32>(12.5); //true +is<float64>(12.5); //true +```` + +## UUID + +UUID v4 wird in der Datenbank üblicherweise als Binary und in JSON als string gespeichert. + +```typescript +import { is, UUID } from '@deepkit/type'; + +is<UUID>('f897399a-9f23-49ac-827d-c16f8e4810a0'); //true +is<UUID>('asd'); //false +``` + +## MongoID + +Markiert dieses Feld als ObjectId für MongoDB. Wird als string aufgelöst. Wird in MongoDB als Binary gespeichert. + +```typescript +import { MongoId, serialize, is } from '@deepkit/type'; + +serialize<MongoId>('507f1f77bcf86cd799439011'); //507f1f77bcf86cd799439011 +is<MongoId>('507f1f77bcf86cd799439011'); //true +is<MongoId>('507f1f77bcf86cd799439011'); //false + +class User { + id: MongoId = ''; //wird in Deepkit ORM automatisch gesetzt, sobald der User eingefügt wurde +} +``` + +## Bigint + +Standardmäßig serialisiert der normale bigint Type als number in JSON (und als long in BSON). Dies hat jedoch Einschränkungen hinsichtlich dessen, was gespeichert werden kann, da bigint in JavaScript eine unbegrenzte potenzielle Größe hat, während numbers in JavaScript und long in BSON begrenzt sind. Um diese Einschränkung zu umgehen, stehen die Types `BinaryBigInt` und `SignedBinaryBigInt` zur Verfügung. + +`BinaryBigInt` ist dasselbe wie bigint, serialisiert in Datenbanken jedoch als unsigniertes Binary mit unbegrenzter Größe (anstatt 8 Bytes in den meisten Datenbanken) und in JSON als string. Negative Werte werden in positive umgewandelt (`abs(x)`). + +```typescript +import { BinaryBigInt } from '@deepkit/type'; + +interface User { + id: BinaryBigInt; +} + +const user: User = { id: 24n }; + +serialize<User>({ id: 24n }); //{id: '24'} + +serialize<BinaryBigInt>(24); //'24' +serialize<BinaryBigInt>(-24); //'0' +``` + +Deepkit ORM speichert BinaryBigInt als Binärfeld. + +`SignedBinaryBigInt` ist dasselbe wie `BinaryBigInt`, kann jedoch auch negative Werte speichern. Deepkit ORM speichert `SignedBinaryBigInt` als Binary. Das Binary hat ein zusätzliches führendes Vorzeichen-Byte und wird als uint dargestellt: 255 für negativ, 0 für null oder 1 für positiv. + +```typescript +import { SignedBinaryBigInt } from '@deepkit/type'; + +interface User { + id: SignedBinaryBigInt; +} +``` + +## MapName + +Um den Namen einer Property in der Serialisierung zu ändern. + +```typescript +import { serialize, deserialize, MapName } from '@deepkit/type'; + +interface User { + firstName: string & MapName<'first_name'>; +} + +serialize<User>({ firstName: 'Peter' }) // {first_name: 'Peter'} +deserialize<User>({ first_name: 'Peter' }) // {firstName: 'Peter'} +``` + +## Group + +Properties können gruppiert werden. Bei der Serialisierung kann man zum Beispiel eine Group von der Serialisierung ausschließen. Siehe das Kapitel Serialisierung für weitere Informationen. + +```typescript +import { serialize } from '@deepkit/type'; + +interface Model { + username: string; + password: string & Group<'secret'> +} + +serialize<Model>( + { username: 'Peter', password: 'nope' }, + { groupsExclude: ['secret'] } +); //{username: 'Peter'} +``` + +## Data + +Jede Property kann zusätzliche Metadaten hinzufügen, die über die Reflection API ausgelesen werden können. Siehe [Laufzeit-Typen-Reflexion](runtime-types.md#runtime-types-reflection) für weitere Informationen. + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface Model { + username: string; + title: string & Data<'key', 'value'> +} + +const reflection = ReflectionClass.from<Model>(); +reflection.getProperty('title').getData()['key']; //value; +``` + +## Excluded + +Jede Property kann für ein bestimmtes Ziel vom Serialisierungsprozess ausgeschlossen werden. + +```typescript +import { serialize, deserialize, Excluded } from '@deepkit/type'; + +interface Auth { + title: string; + password: string & Excluded<'json'> +} + +const item = deserialize<Auth>({ title: 'Peter', password: 'secret' }); + +item.password; //undefined, da der Standard-Serializer von deserialize `json` heißt + +item.password = 'secret'; + +const json = serialize<Auth>(item); +json.password; //wieder undefined, da der Serializer von serialize `json` heißt +``` + +## Embedded + +Markiert das Feld als Embedded Type. + +```typescript +import { PrimaryKey, Embedded, serialize, deserialize } from '@deepkit/type'; + +interface Address { + street: string; + postalCode: string; + city: string; + country: string; +} + +interface User { + id: number & PrimaryKey; + address: Embedded<Address>; +} + +const user: User +{ + id: 12, + address +: + { + street: 'abc', postalCode + : + '1234', city + : + 'Hamburg', country + : + 'Germany' + } +} +; + +serialize<User>(user); +{ + id: 12, + address_street +: + 'abc', + address_postalCode +: + '1234', + address_city +: + 'Hamburg', + address_country +: + 'Germany' +} + +//für deserialize muss die eingebettete Struktur bereitgestellt werden +deserialize<User>({ + id: 12, + address_street: 'abc', + //... +}); +``` + +Es ist möglich, das Präfix zu ändern (standardmäßig ist es der Property-Name). + +```typescript +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: 'addr_' }>; +} + +serialize<User>(user); +{ + id: 12, + addr_street +: + 'abc', + addr_postalCode +: + '1234', +} + +//oder vollständig entfernen +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: '' }>; +} + +serialize<User>(user); +{ + id: 12, + street +: + 'abc', + postalCode +: + '1234', +} +``` + +## Entity + +Um Interfaces mit Entity-Informationen zu annotieren. Wird nur im Datenbankkontext verwendet. + +```typescript +import { Entity, PrimaryKey } from '@deepkit/type'; + +interface User extends Entity<{ name: 'user', collection: 'users'> { + id: number & PrimaryKey; + username: string; +} +``` + +## PrimaryKey + +Markiert das Feld als Primary Key. Wird nur im Datenbankkontext verwendet. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## AutoIncrement + +Markiert das Feld als Auto-Increment. Wird nur im Datenbankkontext verwendet. +Üblicherweise zusammen mit `PrimaryKey`. + +```typescript +import { AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## Reference + +Markiert das Feld als Reference (Foreign Key). Wird nur im Datenbankkontext verwendet. + +```typescript +import { Reference } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; +} +``` + +In diesem Beispiel ist `User.group` eine besitzende Reference, auch bekannt als Foreign Key in SQL. Das bedeutet, dass die `User`-Tabelle eine Spalte `group` hat, die auf die `Group`-Tabelle verweist. Die `Group`-Tabelle ist die Ziel-Tabelle der Reference. + +## BackReference + +Markiert das Feld als Back Reference. Wird nur im Datenbankkontext verwendet. + +```typescript + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; + users: User[] & BackReference; +} +``` + +In diesem Beispiel ist `Group.users` eine Back Reference. Das bedeutet, dass die `User`-Tabelle eine Spalte `group` hat, die auf die `Group`-Tabelle verweist. +Die `Group` hat eine virtuelle Property `users`, die automatisch mit allen Benutzern befüllt wird, die dieselbe `group`-ID wie die `Group`-ID haben, sobald eine Datenbankabfrage +mit Joins ausgeführt wird. Die Property `users` wird nicht in der Datenbank gespeichert. + +## Index + +Markiert das Feld als Index. Wird nur im Datenbankkontext verwendet. + +```typescript +import { Index } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Index; +} +``` + +## Unique + +Markiert das Feld als Unique. Wird nur im Datenbankkontext verwendet. + +```typescript +import { Unique } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Unique; +} +``` + +## DatabaseField + +Mit `DatabaseField` können datenbankspezifische Optionen wie der konkrete Datenbank-Spaltentyp und der Default-Wert usw. definiert werden. + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & DatabaseField<{ type: 'varchar(255)' }>; +} +``` + +## Validation + +TODO + +Siehe [Validierungs-Constraint-Typen](validation.md#validation-constraint-types). + +## InlineRuntimeType + +Um einen Runtime Type inline einzubetten. Wird nur in fortgeschrittenen Fällen verwendet. + +```typescript +import { InlineRuntimeType, ReflectionKind, Type } from '@deepkit/type'; + +const type: Type = { kind: ReflectionKind.string }; + +type Query = { + field: InlineRuntimeType<typeof type>; +} + +const resolved = typeOf<Query>(); // { field: string } +``` + +In TypeScript ist der Type `Query` `{ field: any }`, zur Laufzeit jedoch `{ field: string }`. + +Dies ist nützlich, wenn du ein hochgradig anpassbares System baust, in dem du Runtime Types akzeptierst und sie in verschiedenen anderen Fällen wiederverwendest. + +## ResetAnnotation + +Um alle Annotations einer Property zurückzusetzen. Wird nur in fortgeschrittenen Fällen verwendet. + +```typescript +import { ResetAnnotation } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} + +interface UserCreationPayload { + id: User['id'] & ResetAnnotation<'primaryKey'>; +} +``` + +### Custom Type Annotations + +Du kannst eigene Typannotationen definieren. + +```typescript +type MyAnnotation = { __meta?: ['myAnnotation'] }; +``` + +Konventionsgemäß wird eine Typannotation als Objektliteral mit einer einzelnen optionalen Property `__meta` definiert, die ein Tupel als Type hat. Der erste Eintrag in diesem Tupel ist sein eindeutiger Name und alle nachfolgenden Tupel-Einträge sind beliebige Optionen. Dadurch kann eine Typannotation mit zusätzlichen Optionen ausgestattet werden. + +```typescript +type AnnotationOption<T extends { title: string }> = { __meta?: ['myAnnotation', T] }; +``` + +Die Typannotation wird mit dem Schnittmengen-Operator `&` verwendet. Es können beliebig viele Typannotationen auf einem Type genutzt werden. + +```typescript +type Username = string & MyAnnotation; +type Title = string & MyAnnotation & AnnotationOption<{ title: 'Hello' }>; +``` + +Die Typannotationen können über die Type-Objekte von `typeOf<T>()` und `typeAnnotation` ausgelesen werden: + +```typescript +import { typeOf, typeAnnotation } from '@deepkit/type'; + +const type = typeOf<Username>(); +const annotation = typeAnnotation.getForName(type, 'myAnnotation'); //[] +``` + +Das Ergebnis in `annotation` ist entweder ein Array mit Optionen, falls die Typannotation `myAnnotation` verwendet wurde, oder `undefined`, falls nicht. Wenn die Typannotation zusätzliche Optionen hat, wie in `AnnotationOption` zu sehen, finden sich die übergebenen Werte im Array. +Bereits mitgelieferte Typannotationen wie `MapName`, `Group`, `Data` usw. haben ihr eigenes Annotation-Objekt: + +```typescript +import { typeOf, Group, groupAnnotation } from '@deepkit/type'; + +type Username = string & Group<'a'> & Group<'b'>; + +const type = typeOf<Username>(); +groupAnnotation.getAnnotations(type); //['a', 'b'] +``` + +Siehe [Laufzeit-Typen-Reflexion](./reflection.md), um mehr zu erfahren. \ No newline at end of file diff --git a/website/src/translations/de/documentation/runtime-types/validation.md b/website/src/translations/de/documentation/runtime-types/validation.md new file mode 100644 index 000000000..5a50bc11e --- /dev/null +++ b/website/src/translations/de/documentation/runtime-types/validation.md @@ -0,0 +1,549 @@ +# Validierung + +Validierung ist der systematische Prozess, Daten auf Genauigkeit und Integrität zu überprüfen. Dabei geht es nicht nur darum zu prüfen, ob der Datentyp dem erwarteten Typ entspricht, sondern auch, ob zusätzliche vordefinierte Constraints erfüllt sind. + +Validierung wird besonders wichtig, wenn man mit Daten aus unsicheren oder nicht vertrauenswürdigen Quellen arbeitet. Eine „unsichere“ Quelle ist eine, bei der Typen oder Inhalte der Daten unvorhersehbar sind und zur Laufzeit potenziell beliebige Werte annehmen können. Typische Beispiele sind Benutzereingaben, Daten aus HTTP-Requests (wie Query-Parameter oder der Body), CLI-Argumente oder Dateien, die in ein Programm eingelesen werden. Solche Daten sind inhärent riskant, da falsche Typen oder Werte Programmfehler verursachen oder sogar Sicherheitslücken einführen können. + +Wenn beispielsweise eine Variable eine Zahl speichern soll, ist es entscheidend zu validieren, dass sie tatsächlich einen numerischen Wert enthält. Ein Mismatch kann zu unerwarteten Abstürzen oder Sicherheitsverletzungen führen. + +Bei der Gestaltung eines HTTP-Route-Controllers muss etwa die Validierung aller Benutzereingaben Priorität haben, sei es über Query-Parameter, den Request-Body oder andere Wege. Besonders in Umgebungen mit TypeScript ist es wichtig, Type Casts zu vermeiden. Diese Casts können irreführend sein und grundlegende Sicherheitsrisiken einführen. + +```typescript +app.post('/user', function(request) { + const limit = request.body.limit as number; +}); +``` + +Ein häufig auftretender Fehler beim Programmieren sind Type Casts, die zur Laufzeit keine Sicherheit bieten. Wenn Sie beispielsweise eine Variable als Zahl casten, aber ein Benutzer eine Zeichenkette eingibt, wird das Programm dazu verleitet, so zu arbeiten, als wäre die Zeichenkette eine Zahl. Solche Versäumnisse können Systemabstürze verursachen oder ernsthafte Sicherheitsrisiken darstellen. Um diese Risiken zu minimieren, können Entwickler Validatoren und Type Guards nutzen. Zusätzlich können Serializer dabei helfen, Variablen zu konvertieren, etwa indem sie 'limit' in eine Zahl umwandeln. Weitere Einblicke hierzu finden Sie im Abschnitt zur Serialization. + +Validierung ist nicht nur eine Option; sie ist ein integraler Bestandteil soliden Software-Designs. Es ist immer klug, auf Nummer sicher zu gehen: lieber zu viel validieren als später unzureichende Prüfungen zu bereuen. Deepkit versteht diese Bedeutung und bietet eine Fülle an Validierungswerkzeugen. Zudem sorgt das High-Performance-Design für minimale Auswirkungen auf die Ausführungszeit. Als Leitprinzip gilt: Setzen Sie umfassende Validierung ein, um Ihre Anwendung zu schützen, auch wenn es sich mitunter redundant anfühlt. + +Viele Komponenten von Deepkit, einschließlich des HTTP-Routers, der RPC-Abstraktion und sogar der Datenbankabstraktion, verfügen über eingebaute Validierungssysteme. Diese Mechanismen werden automatisch ausgelöst, was oft die Notwendigkeit manueller Eingriffe eliminiert. + + +Für ein umfassendes Verständnis, wann und wie automatische Validierung erfolgt, verweisen wir auf die spezifischen Kapitel ([CLI](../cli.md), [HTTP](../http.md), [RPC](../rpc.md), [ORM](../orm.md)). +Machen Sie sich mit den notwendigen Constraints und Datentypen vertraut. Korrekt definierte Parameter erschließen Deepkits automatisches Validierungspotenzial, reduzieren manuelle Arbeit und sorgen für saubereren, sichereren Code. + +## Verwendung + +Die grundlegende Funktion des Validators besteht darin, einen Wert auf seinen Typ zu prüfen, zum Beispiel, ob ein Wert eine string ist. Es geht nicht darum, was der string enthält, sondern nur um seinen Typ. In TypeScript gibt es viele Typen: string, number, boolean, bigint, objects, classes, interface, generics, mapped types und viele mehr. Aufgrund des leistungsstarken Typsystems von TypeScript steht eine große Vielfalt verschiedener Typen zur Verfügung. + +In JavaScript selbst können primitive Typen mit dem Operator `typeof` geprüft werden. Für komplexere Typen wie interfaces, mapped types oder generic set/map ist das nicht mehr so einfach und eine Validator-Bibliothek wie `@deepkit/type` wird notwendig. Deepkit ist die einzige Lösung, die es ermöglicht, alle TypeScript-Typen direkt und ohne Workarounds zu validieren. + + + +In Deepkit kann die Typvalidierung entweder mit der Funktion `validate`, `is` oder `assert` erfolgen. +Die Funktion `is` ist ein sogenannter Type Guard und `assert` ist eine Type Assertion. Beide werden im nächsten Abschnitt erläutert. +Die Funktion `validate` gibt ein Array gefundener Fehler zurück und bei Erfolg ein leeres Array. Jeder Eintrag in diesem Array beschreibt den genauen Fehlercode und die Fehlermeldung sowie den Pfad, wenn komplexere Typen wie Objekte oder Arrays validiert werden. + +Alle drei Funktionen werden in etwa auf die gleiche Weise verwendet. Der Typ wird als erster Typargument angegeben oder referenziert und die Daten als erstes Funktionsargument übergeben. + +```typescript +import { validate, is, assert } from '@deepkit/type'; + +const errors = validate<string>('abc'); //[] +const errors = validate<string>(123); //[{code: 'type', message: 'Not a string'}] + +if (is<string>(value)) { + // value ist garantiert eine string +} + +function doSomething(value: any) { + assert<string>(value); //wirft bei ungültigen Daten + + // value ist garantiert eine string +} +``` + +Wenn Sie mit komplexeren Typen wie classes oder interfaces arbeiten, kann das Array auch mehrere Einträge enthalten. + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>(undefined); //[{code: 'type', message: 'Not a object'}] + +validate<User>({}); +//[ +// {path: 'id', code: 'type', message: 'Not a number'}], +// {path: 'username', code: 'type', message: 'Not a string'}], +//] +``` + +Der Validator unterstützt auch tief rekursive Typen. Pfade werden dann mit einem Punkt getrennt. + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; + supervisor?: User; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>({id: 1, username: 'Joe', supervisor: {}}); +//[ +// {path: 'supervisor.id', code: 'type', message: 'Not a number'}], +// {path: 'supervisor.username', code: 'type', message: 'Not a string'}], +//] +``` + +Nutzen Sie die Vorteile, die TypeScript bietet. Komplexere Typen wie ein `User` können beispielsweise an mehreren Stellen wiederverwendet werden, ohne sie immer wieder deklarieren zu müssen. Wenn ein `User` ohne seine `id` validiert werden soll, können TypeScript-Utilities verwendet werden, um schnell und effizient abgeleitete Subtypen zu erstellen. Ganz im Sinne von DRY (Don't Repeat Yourself). + +```typescript +type UserWithoutId = Omit<User, 'id'>; + +validate<UserWithoutId>({username: 'Joe'}); //gültig! +``` + +Deepkit ist das einzige große Framework, das die Fähigkeit hat, zur Laufzeit auf TypeScripts Typen in dieser Weise zuzugreifen. Wenn Sie Typen im Frontend und Backend verwenden möchten, können Typen in eine separate Datei ausgelagert und überall importiert werden. Nutzen Sie diese Möglichkeit zu Ihrem Vorteil, um den Code effizient und sauber zu halten. + +## Type Casts sind unsicher + +Ein Type Cast (im Gegensatz zum Type Guard) ist in TypeScript kein Konstrukt zur Laufzeit, sondern wird nur im Typsystem selbst verarbeitet. Es ist kein sicherer Weg, einem unbekannten Datum einen Typ zuzuweisen. + +```typescript +const data: any = ...; + +const username = data.username as string; + +if (username.startsWith('@')) { //könnte abstürzen +} +``` + +Der Code `as string` ist nicht sicher. Die Variable `data` könnte buchstäblich jeden Wert haben, zum Beispiel `{username: 123}` oder sogar `{}`, mit der Folge, dass `username` kein string ist, sondern etwas völlig anderes. Der Code `username.startsWith('@')` führt dann zu einem Error, sodass im harmloseren Fall das Programm abstürzt und im schlimmsten Fall eine Sicherheitslücke entsteht. +Um zur Laufzeit zu garantieren, dass `data` hier eine Property `username` mit dem Typ string hat, müssen Type Guards verwendet werden. + +Type Guards sind Funktionen, die TypeScript einen Hinweis darauf geben, welchen Typ die übergebenen Daten zur Laufzeit garantiert haben. Mit diesem Wissen „verengt“ TypeScript den Typ im weiteren Codeverlauf. Aus `any` kann so auf sichere Weise eine string oder ein anderer Typ werden. Wenn es also Daten gibt, deren Typ nicht bekannt ist (`any` oder `unknown`), hilft ein Type Guard, ihn anhand der Daten selbst genauer einzugrenzen. Allerdings ist der Type Guard nur so sicher wie seine Implementierung. Wenn dabei ein Fehler passiert, kann das schwerwiegende Folgen haben, weil grundlegende Annahmen plötzlich als falsch herausstellen. + +<a name="type-guard"></a> + +## Type-Guard + +Ein Type Guard für den oben verwendeten Typ `User` könnte in der einfachsten Form wie folgt aussehen. Beachten Sie, dass die oben erläuterten Besonderheiten mit NaN hier nicht enthalten sind und dieser Type Guard daher nicht ganz korrekt ist. + +```typescript +function isUser(data: any): data is User { + return 'object' === typeof data + && 'number' === typeof data.id + && 'string' === typeof data.username; +} + +isUser({}); //false + +isUser({id: 1, username: 'Joe'}); //true +``` + +Ein Type Guard gibt immer ein boolean zurück und wird üblicherweise direkt in einer If-Operation verwendet. + +```typescript +const data: any = await fetch('/user/1'); + +if (isUser(data)) { + data.id; //kann sicher zugegriffen werden und ist eine number +} +``` + +Für jeden Type Guard eine eigene Funktion zu schreiben, insbesondere für komplexere Typen, und diese dann jedes Mal anzupassen, wenn sich ein Typ ändert, ist extrem mühsam, fehleranfällig und ineffizient. Daher stellt Deepkit die Funktion `is` bereit, die automatisch einen Type Guard für jeden TypeScript-Typ liefert. Diese berücksichtigt dann auch automatisch Besonderheiten wie das oben erwähnte Problem mit NaN. Die Funktion `is` macht das Gleiche wie `validate`, gibt aber statt eines Arrays von Fehlern einfach ein boolean zurück. + +```typescript +import { is } from '@deepkit/type'; + +is<string>('abc'); //true +is<string>(123); //false + + +const data: any = await fetch('/user/1'); + +if (is<User>(data)) { + //data ist jetzt garantiert vom Typ User +} +``` + +Ein häufig verwendetes Muster ist, bei fehlerhafter Validierung direkt einen Error zurückzugeben, sodass nachfolgender Code nicht ausgeführt wird. Dies kann an verschiedenen Stellen verwendet werden, ohne den gesamten Codefluss zu ändern. + +```typescript +function addUser(data: any): void { + if (!is<User>(data)) throw new TypeError('No user given'); + + //data ist jetzt garantiert vom Typ User +} +``` + +Alternativ kann eine TypeScript Type Assertion verwendet werden. Die Funktion `assert` wirft automatisch einen Error, wenn die übergebenen Daten nicht korrekt zu einem Typ validieren. Die spezielle Signatur der Funktion, die TypeScript Type Assertions auszeichnet, hilft TypeScript, die übergebene Variable automatisch zu verengen. + +```typescript +import { assert } from '@deepkit/type'; + +function addUser(data: any): void { + assert<User>(data); //wirft bei ungültigen Daten + + //data ist jetzt garantiert vom Typ User +} +``` + +Auch hier gilt: Nutzen Sie die Vorteile, die TypeScript bietet. Typen können mithilfe verschiedener TypeScript-Funktionen wiederverwendet oder angepasst werden. + +<a name="error-reporting"></a> + +## Fehlermeldungen + +Die Funktionen `is`, `assert` und `validates` liefern ein boolean als Ergebnis. Um genaue Informationen über fehlgeschlagene Validierungsregeln zu erhalten, kann die Funktion `validate` verwendet werden. Sie gibt ein leeres Array zurück, wenn alles erfolgreich validiert wurde. Im Fehlerfall enthält das Array einen oder mehrere Einträge mit folgender Struktur: + +```typescript +interface ValidationErrorItem { + /** + * Der Pfad zur Property. Kann ein tiefer Pfad sein, getrennt durch Punkt. + */ + path: string; + /** + * Ein kleingeschriebener Fehlercode, der zur Identifikation und Übersetzung genutzt werden kann. + */ + code: string, + /** + * Freitext der Fehlermeldung. + */ + message: string, +} +``` + +Die Funktion erhält als erstes Typargument jeden beliebigen TypeScript-Typ und als erstes Argument die zu validierenden Daten. + +```typescript +import { validate } from '@deepkit/type'; + +validate<string>('Hello'); //[] +validate<string>(123); //[{code: 'type', message: 'Not a string', path: ''}] + +validate<number>(123); //[] +validate<number>('Hello'); //[{code: 'type', message: 'Not a number', path: ''}] +``` + +Komplexe Typen wie interfaces, classes oder generics können ebenfalls verwendet werden. + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>(undefined); //[{code: 'type', message: 'Not an object', path: ''}] +validate<User>({}); //[{code: 'type', message: 'Not a number', path: 'id'}] +validate<User>({id: 1}); //[{code: 'type', message: 'Not a string', path: 'username'}] +validate<User>({id: 1, username: 'Peter'}); //[] +``` + +<a name="constraints"></a> + +## Constraints + +Zusätzlich zur Typprüfung können einem Typ weitere beliebige Constraints hinzugefügt werden. Die Validierung dieser zusätzlichen inhaltlichen Constraints erfolgt automatisch, nachdem die Typen selbst validiert wurden. Dies geschieht in allen Validierungsfunktionen wie `validate`, `is` und `assert`. +Eine Constraint kann zum Beispiel sein, dass ein string eine bestimmte minimale oder maximale Länge haben muss. Diese Constraints werden über [Type Annotations](./types.md) zu den eigentlichen Typen hinzugefügt. Es gibt eine ganze Reihe von Annotations, die verwendet werden können. Eigene Annotations können bei erweitertem Bedarf beliebig definiert und genutzt werden. + +```typescript +import { MinLength } from '@deepkit/type'; + +type Username = string & MinLength<3>; +``` + +Mit `&` können beliebig viele Type Annotations zum eigentlichen Typ hinzugefügt werden. Das Ergebnis, hier `username`, kann dann in allen Validierungsfunktionen, aber auch in anderen Typen verwendet werden. + +```typescript +import { is } from '@deepkit/type'; + +is<Username>('ab'); //false, weil die Mindestlänge 3 ist +is<Username>('Joe'); //true + +interface User { + id: number; + username: Username; +} + +is<User>({id: 1, username: 'ab'}); //false, weil die Mindestlänge 3 ist +is<User>({id: 1, username: 'Joe'}); //true +``` + +Die Funktion `validate` liefert hilfreiche Fehlermeldungen, die von den Constraints kommen. + +```typescript +import { validate } from '@deepkit/type'; + +const errors = validate<Username>('xb'); +//[{ code: 'minLength', message: `Min length is 3` }] +``` + +Diese Informationen können zum Beispiel hervorragend auch automatisch in einem Formular dargestellt und mittels `code` übersetzt werden. Durch den vorhandenen Pfad für Objekte und Arrays können Felder in einem Formular die passende Fehlermeldung herausfiltern und anzeigen. + +```typescript +validate<User>({id: 1, username: 'ab'}); +//{ path: 'username', code: 'minLength', message: `Min length is 3` } +``` + +Ein häufig nützlicher Anwendungsfall ist auch, eine E-Mail mit einem RegExp-Constraint zu definieren. Sobald der Typ definiert ist, kann er überall verwendet werden. + +```typescript +export const emailRegexp = /^\S+@\S+$/; +type Email = string & Pattern<typeof emailRegexp> + +is<Email>('abc'); //false +is<Email>('joe@example.com'); //true +``` + +Es können beliebig viele Constraints hinzugefügt werden. + +```typescript +type ID = number & Positive & Maximum<1000>; + +is<ID>(-1); //false +is<ID>(123); //true +is<ID>(1001); //true +``` + +### Constraint-Typen + +#### Validate<typeof myValidator> + +Validierung mit einer benutzerdefinierten Validator-Funktion. Weitere Informationen siehe nächster Abschnitt Custom Validator. + +```typescript +import { ValidatorError, Validate } from '@deepkit/type'; + +function startsWith(v: string) { + return (value: any) => { + const valid = 'string' === typeof value && value.startsWith(v); + return valid ? undefined : new ValidatorError('startsWith', `Does not start with ${v}`); + }; +} + +type T = string & Validate<typeof startsWith, 'abc'>; +``` + +#### Pattern<typeof myRegexp> + +Definiert einen regulären Ausdruck als Validierungs-Pattern. Wird normalerweise für E-Mail-Validierung oder komplexere Inhaltsvalidierungen verwendet. + +```typescript +import { Pattern } from '@deepkit/type'; + +const myRegExp = /[a-zA-Z]+/; +type T = string & Pattern<typeof myRegExp> +``` + +#### Alpha + +Validierung für Alpha-Zeichen (a–Z). + +```typescript +import { Alpha } from '@deepkit/type'; + +type T = string & Alpha; +``` + + +#### Alphanumeric + +Validierung für alphanumerische Zeichen. + +```typescript +import { Alphanumeric } from '@deepkit/type'; + +type T = string & Alphanumeric; +``` + + +#### Ascii + +Validierung für ASCII-Zeichen. + +```typescript +import { Ascii } from '@deepkit/type'; + +type T = string & Ascii; +``` + + +#### Decimal<number, number> + +Validierung für strings, die eine Dezimalzahl repräsentieren, wie 0.1, .3, 1.1, 1.00003, 4.0, etc. + +```typescript +import { Decimal } from '@deepkit/type'; + +type T = string & Decimal<1, 2>; +``` + + +#### MultipleOf<number> + +Validierung von Zahlen, die ein Vielfaches der angegebenen Zahl sind. + +```typescript +import { MultipleOf } from '@deepkit/type'; + +type T = number & MultipleOf<3>; +``` + + +#### MinLength<number>, MaxLength<number>, MinMax<number, number> + +Validierung für minimale/maximale Länge bei Arrays oder strings. + +```typescript +import { MinLength, MaxLength, MinMax } from '@deepkit/type'; + +type T = any[] & MinLength<1>; + +type T = string & MinLength<3> & MaxLength<16>; + +type T = string & MinMax<3, 16>; +``` + +#### Includes<'any'> Excludes<'any'> + +Validierung dafür, dass ein Array-Item oder Substring enthalten/ausgeschlossen ist + +```typescript +import { Includes, Excludes } from '@deepkit/type'; + +type T = any[] & Includes<'abc'>; +type T = string & Excludes<' '>; +``` + +#### Minimum<number>, Maximum<number> + +Validierung für einen Wert, der mindestens oder höchstens eine angegebene Zahl ist. Entspricht `>=` und `<=`. + +```typescript +import { Minimum, Maximum, MinMax } from '@deepkit/type'; + +type T = number & Minimum<10>; +type T = number & Minimum<10> & Maximum<1000>; + +type T = number & MinMax<10, 1000>; +``` + +#### ExclusiveMinimum<number>, ExclusiveMaximum<number> + +Wie Minimum/Maximum, schließt aber den Wert selbst aus. Entspricht `>` und `<`. + +```typescript +import { ExclusiveMinimum, ExclusiveMaximum } from '@deepkit/type'; + +type T = number & ExclusiveMinimum<10>; +type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>; +``` + + +#### Positive, Negative, PositiveNoZero, NegativeNoZero + +Validierung dafür, dass ein Wert positiv oder negativ ist. + +```typescript +import { Positive, Negative } from '@deepkit/type'; + +type T = number & Positive; +type T = number & Negative; +``` + + +#### BeforeNow, AfterNow + +Validierung für einen Datumswert im Vergleich zu jetzt (new Date). + +```typescript +import { BeforeNow, AfterNow } from '@deepkit/type'; + +type T = Date & BeforeNow; +type T = Date & AfterNow; +``` + +#### Email + +Einfache RegExp-Validierung von E-Mails via `/^\S+@\S+$/`. Ist automatisch ein `string`, daher kein `string & Email` nötig. + +```typescript +import { Email } from '@deepkit/type'; + +type T = Email; +``` + +#### integer + +Stellt sicher, dass die number eine Ganzzahl im korrekten Bereich ist. Ist automatisch eine `number`, daher kein `number & integer` nötig. + +```typescript +import { integer, uint8, uint16, uint32, + int8, int16, int32 } from '@deepkit/type'; + +type T = integer; +type T = uint8; +type T = uint16; +type T = uint32; +type T = int8; +type T = int16; +type T = int32; +``` + +Siehe Special types: integer/floats für weitere Informationen + +### Custom Validator + +Wenn die integrierten Validatoren nicht ausreichen, können benutzerdefinierte Validierungsfunktionen erstellt und über den `Validate`-Decorator verwendet werden. + +```typescript +import { ValidatorError, Validate, Type, validates, validate } + from '@deepkit/type'; + +function titleValidation(value: string, type: Type) { + value = value.trim(); + if (value.length < 5) { + return new ValidatorError('tooShort', 'Value is too short'); + } +} + +interface Article { + id: number; + title: string & Validate<typeof titleValidation>; +} + +console.log(validates<Article>({id: 1})); //false +console.log(validates<Article>({id: 1, title: 'Peter'})); //true +console.log(validates<Article>({id: 1, title: ' Pe '})); //false +console.log(validate<Article>({id: 1, title: ' Pe '})); //[ValidationErrorItem] +``` + +Beachten Sie, dass Ihre benutzerdefinierte Validierungsfunktion ausgeführt wird, nachdem alle integrierten Typ-Validatoren aufgerufen wurden. Wenn ein Validator fehlschlägt, werden alle nachfolgenden Validatoren für den aktuellen Typ übersprungen. Pro Typ ist nur ein Fehler möglich. + +#### Generischer Validator + +In der Validator-Funktion steht das Typobjekt zur Verfügung, mit dem weitere Informationen über den Typ abgerufen werden können, der den Validator verwendet. Es besteht auch die Möglichkeit, eine beliebige Validator-Option zu definieren, die dem Validate-Typ übergeben werden muss und den Validator konfigurierbar macht. Mit diesen Informationen und ihren Parent-Referenzen können leistungsfähige generische Validatoren erstellt werden. + +```typescript +import { ValidatorError, Validate, Type, is, validate } + from '@deepkit/type'; + +function startsWith(value: any, type: Type, chars: string) { + const valid = 'string' === typeof value && value.startsWith(chars); + if (!valid) { + return new ValidatorError('startsWith', 'Does not start with ' + chars) + } +} + +type MyType = string & Validate<typeof startsWith, 'a'>; + +is<MyType>('aah'); //true +is<MyType>('nope'); //false + +const errors = validate<MyType>('nope'); +//[{ path: '', code: 'startsWith', message: `Does not start with a` }]); +``` \ No newline at end of file diff --git a/website/src/translations/de/state.json b/website/src/translations/de/state.json new file mode 100644 index 000000000..73e29dfb6 --- /dev/null +++ b/website/src/translations/de/state.json @@ -0,0 +1,116 @@ +{ + "index.md": "29488f37f0c90cb66e32023aa3869f6081dab42590def9196514de8e51e977de", + "introduction.md": "7159b93c7cf42ed98bd0a86ed2b0100884e1eae755e56d8392e9a4dfbe306dd0", + "app.md": "f1f4383a395605c4cd1c36314b61afeebc6369e8c624080ba4f005bd064db99d", + "app/arguments.md": "009c2438d897ea0f25d569d9b3b9654e55d1a01ad3c04bf8b4c512a1b9789f59", + "app/dependency-injection.md": "b89fd5d14b2e0180a5c29f31e79ecc4112d328d9efe89f4e5a3ca8caa88129f5", + "app/modules.md": "9d0c92e9db99c4e96c00152a51f69efa49c974860e33937aec6144d10795dc27", + "app/services.md": "9df90a136ffed203714a4427137ba9bfda6901eb77c28dfe0d8c305f135c06c3", + "app/events.md": "45af019cd3a41e6e5c4987b6d7980d5103afb20a889aba7c9a8d04cae73b3dba", + "app/logger.md": "467d21c830908bc4a49c3a3d80dd6c9207c3403977a693788990b04d89f2c8d6", + "app/configuration.md": "5cd9710e8c7037e4af0a43f47095332fca70a9c0a1e267f249d0f89e1d0c995e", + "framework.md": "7d23eb8115535150c547adc60d8677c9e74f7aa6d0f9ba2aa2c34dd3c61c1f72", + "framework/database.md": "3082875ba15ff97becd94966634dc57a4e3a96e0e09db1bd1a901faeecfc3a9f", + "framework/testing.md": "f22048a69772b752d7174645e25708288f67721ac4f16774e7c3786d9143691f", + "framework/deployment.md": "9f43e70a78caf28a58a76e6b6f4b654dca8e83fef9a5dfacd0544c9a40251ee0", + "framework/public.md": "014cf4e0702d543c6aa086a1256e024d63a06fe761f4352e06ba953a3ae5d39f", + "runtime-types.md": "ddb2b9e67054ede0ca0937044b4876882326ea421d7a9919633ae8187782bff7", + "runtime-types/getting-started.md": "437cb0b8ba80b036e023719d8bd56ad22d5414420199b9fa7561afba1f68b65a", + "runtime-types/types.md": "f8b43c9534f850990aee6e57dfcd5bffdaf087d953b2edc3f95dea5b9cc53b6d", + "runtime-types/reflection.md": "5b96135eeb9281f65ca5c365d4697868a31ce2a85b141e0283070ac3bec8dae9", + "runtime-types/serialization.md": "9d1d7adac33dc116244b6edb7cf37c88c7d7657c3692d3e354e04c48b71d7561", + "runtime-types/validation.md": "9ba4b9f65b3f2c882e1df373411652df407b1ee9300faed54d6f4b62b4a0f986", + "runtime-types/extend.md": "2b88f476dac2c88ffcd05719177b9c5e43281b65cd3925698df14483f3de4792", + "runtime-types/custom-serializer.md": "b3da230cee1630ea1eb7d89f7b3e241726c3b956dffb6a8a3f94da0a2ea8ef12", + "runtime-types/external-types.md": "216d15bd8b956f8821bd1c3c86571550beabe9b769237514b232b4dd8af72714", + "runtime-types/bytecode.md": "b22322df3dcc093da4d776a1b68d3d11ae5e8c8053723f785ad2a25c6a2e8d2e", + "dependency-injection.md": "c1ddf1c3c6dc56783c7e282605f5b3b0ea50f81cd8d614ff77a6459e3893f3d4", + "dependency-injection/getting-started.md": "4a381a82a5b5aa208c44d08334b6b7ae8ee129878f1a212b08230371cee99556", + "dependency-injection/providers.md": "64ae199092c89687a393ec3b50722c746e1048fb9aceed2c5f4d61150c43c573", + "dependency-injection/injection.md": "399be234a76ea3575f53a1c1bf04ab7368b4d9711b3811f5be69ccdb275da80c", + "dependency-injection/configuration.md": "9f84fa97310995897afafdf2b6b5376858ad22722d25a19562b1f70051a41009", + "dependency-injection/scopes.md": "7886d8e715ef18a5b058a89fe099cf4f5ce2947495c7f1fb4a8fe1908ec0cf59", + "filesystem.md": "97fc7c9f028efb934b9ad864135420007cd1910c7b8a7b3b8bdc86b0b8e90cdf", + "filesystem/app.md": "f7aa47e42604191de2d549f1f8e5e9582394e55da0c8d470147783b62a6a383d", + "filesystem/local.md": "e62660fe771d5c22c1414a055bc96e77a92da04e780f8853be9a5d327a2077b2", + "filesystem/memory.md": "67e061e990066271c4e6d92e1552319a8f1d3e2b5b97b2a26a9dc2e88858e906", + "filesystem/database.md": "8d1132ef3880a90e48e27598514ce6f75eed6328658e7c29b3af9509b8735859", + "filesystem/aws-s3.md": "989c857dbe659e4c6a765442143945402ed49db36248958a123c3d11649b4148", + "filesystem/ftp.md": "3400d186b86e1abb2823a0d64a143720099b2d5ba9c9e59e78ce1cc24d74df65", + "filesystem/sftp.md": "f54ebc5db81bf8cc4e3e21e922168db5456ba9912abbcb456a17d7603ffee4db", + "filesystem/google-storage.md": "c20d8abfc8e10c26844bc8a154ca8eacdf202c812cdde45911e30086943429cb", + "broker.md": "0e97c1ea8ba83e929555018e47a2e7ea9a4d951a2eda3fdd3c68ca2e83dc9c99", + "broker/cache.md": "cfbc3dc50876e5951c848a60a82c5b83d34dc351cb325a64850e6910a774ba34", + "broker/message-bus.md": "8fec0f694e08bd4e894ddafa8d97ce4183b8713d1d9be93dcb1bde4df7ac700c", + "broker/message-queue.md": "cbc54b3d2ea7aba249948f06d8f622defe7cfcae9ef5e0fa48f97b876e8346fb", + "broker/atomic-locks.md": "90703416ba1fec9f5c14eee4972ee78b983d126544737f2801188962741d87c8", + "broker/key-value.md": "da10a448f38971d72963500f4cfc06ad8253d79808d877453f6e5734cdf30070", + "http.md": "2c20d0e955afe136ada302e197f176255aac7d941760c208f0dc75c2f3cf258a", + "http/getting-started.md": "59605a5fe0ca617bb70e3fd0014340b9b57a88e8e91fac9bdc3eaf0a8f8f11c1", + "http/input-output.md": "c225ccfafb21816bd804a96110e69301baa0dbb57304baed8a7595b771f46f77", + "http/views.md": "62ce86b45d849b7946608aeea23f88f1078d74f971703709c98f8ad476944893", + "http/dependency-injection.md": "65d42f23a838af794cc43e60aaa2ae6cd3d72e2d0fc275065e65c6c639a6e534", + "http/events.md": "aa47d884f82e724f1dcb5b33b10624bef1e9a04b84f97c4db5a8d3f8da6bd33b", + "http/middleware.md": "9485a78862a08ff1de9cfd52b89f095676e70e058ec732b8e5617d96e961372a", + "http/security.md": "cde7cb77eb3b2aa51456d20d29a06ea7ab205b40ef9ad586dbfd136cd498019c", + "rpc.md": "cf15217c23ae52a86ab7dcf628c668d5d06b9720ef4ddb3717f02994a3d507f7", + "rpc/getting-started.md": "b09000d9e2d9dfe6527b8a42cbec75ae2206284d4395b75e21a8e1c596430ba9", + "rpc/dependency-injection.md": "c8e5218600779f3c3c4fcdccbe7ab8add343f8a8c7f2847dc3b80da96d1604e9", + "rpc/security.md": "790bb3ff8ef236250d29d16b55e8b685be273f52accb3a368be71eed4a8e639d", + "rpc/errors.md": "7785519ce38ddd349353e71bcc62bb5ba74582f52c17378508f4c6e9051609a9", + "rpc/transport.md": "9561d6ccbfeaf511b6a914fd3d7e6f4b84411f70938ad639665f113e469e1991", + "orm.md": "e922d5a0e28aa3753e5332418f2802f1ae62596d17ae96f46d3deca59c4e7c0b", + "orm/getting-started.md": "56e4f565f748aaf043ce8710fc5a58402fb5889366d1478a267e26628ff4e0f6", + "orm/entity.md": "5fccac031df6723dc0e6bdc9ef7ecd4268631b3b14854661d8ce6dc918f9959c", + "orm/session.md": "88c774506bcca58e5c272f59b844846ebe4efe5ab2e3a367528764b0dc5dfdee", + "orm/query.md": "540a355c2c1666c2a4d9e7c8cc202994b732b17408508fd6944dde1f62a99646", + "orm/transactions.md": "b843d35bab46190b24a79f05950b6a301349bf539c01aae7b90716f5d0153b41", + "orm/inheritance.md": "943f367861fcaebffe70d16c4d795a739b16537bed0ca602d0343e0684ebc0ae", + "orm/relations.md": "9cfd7b3516d704e091207b0d78995b26df6d5b88b1c8181ee021ee0d7767795a", + "orm/events.md": "220d9066075c3a82e354dc8e6fcf82db649e440b8c3df0c0d2722ac5bec9dcfd", + "orm/migrations.md": "2ee65d0ccd58635e9cd115a3abf83b2488ddc925a7d5f5b25d8844bcedc107e6", + "orm/orm-browser.md": "ee8940fa282c2d42861ce203524277f4672c880a3e79cffdc978c55e795cec1b", + "orm/raw-access.md": "55c6e5a40386cff597293293536be5b47b082122432156b50bf06b386d3bb694", + "orm/seeding.md": "92bad8e262e1afdc818b39e6aa91c50a9016ad37da273defb7caf575821e6539", + "orm/composite-primary-key.md": "155cb42a4dcd9deffaa67aa30369d86c37826a28740977fe1905ef0c61bb9129", + "orm/plugin-soft-delete.md": "afa2c7f42833bddda5f7e116a3e7eabfc7183d112812e6c4e8c3a802aca5c3ff", + "package/angular-ssr.md": "e7e74dfdd8f957e82061c449938c2e1288031b976921e54e28b4f0d35b5d491d", + "package/api-console.md": "82d5322b37c517da883f567a18d4f64ecbcd61570300ac337e5e3c1473359880", + "package/app.md": "e60ef9f596cc6df26f490350a0001a912a715d463c762ed09ca6bf796e4cb4b3", + "package/bench.md": "f7a06c685a2f05aa397a2f271bec2a98c729c4273591e6996381b420a57116df", + "package/broker.md": "e46abbefd581c101d77fdf35151eddeeba32074d24f6e7b737841c3cefa2b76f", + "package/broker-redis.md": "67bbf67a50265ada9755bf093cc3d566b8ff21dd10a2cd1f3ed5f589826aac1a", + "package/bson.md": "dbbadeca42bacda8a7cb2e7cce55e9627463537cf16de4458dd029163575063e", + "package/bun.md": "a5627944c66b654c3b64e380fa58acebf0a22f4b045ae218880d882973f457eb", + "package/core.md": "fabeee88c765e0a0713cfafa7cc502bd4f04fa4c69467a8a9985d7d74b5b963e", + "package/core-rxjs.md": "42784cd0fa8c85d0473753e1cee7b769bf03d4d220f18add821bfa402624619f", + "package/devtool.md": "9fa98e55994ae65e50032d11c9c01f7fb52fbe0dec22cc9b2b6d5dbbe67af122", + "package/event.md": "4bcb6f33e7b788d4f2db94f8871ccded00c6523cd80bcf593d732293d207e148", + "package/filesystem.md": "0c2634e9f2f938d1bec04d201e449642f2573c1e0790c1fb783dccea91b4fa63", + "package/filesystem-aws-s3.md": "40cfd7a7cd0d0f495d9e76f693792859accd6e56a92d6defbff0458cd55ee73d", + "package/filesystem-database.md": "61848eaf206556fbc5d317eb7a1286288bc48329ad209b7dc4ce6daaa1ce374f", + "package/filesystem-ftp.md": "fdbd9659eb784a5f7cc8219a59c647b7646c7de939e424fe23ea9c6fe0306d3a", + "package/filesystem-google.md": "549623afd3bfa25d793758893360902751a21b0b6ae900e27448c8cb2bc8593c", + "package/filesystem-sftp.md": "b2966556da65453da7b98a6900667b2fb97bab48c986bbdae667c172fec44b41", + "package/framework.md": "027c9d986ec7e32a3b93a857f4cdb80367d17b54faf59c7d1620d8a21f7c4628", + "package/http.md": "159c8f0553a1cd8f71598ef7128a78525b7ad55999b8b2270acbdd05435352fd", + "package/injector.md": "f01def47d678021bb90a48e20d7e94ce18e865422affcf39314d66d04218b11e", + "package/logger.md": "0336bedc41fda399389fbb9085032091312973f33e3ec9bd4ed46aa931469100", + "package/mongo.md": "359d07c8c17320d28840fbdfa0c3eb219f5eedd65b14866ab3ac64d25d502c6f", + "package/mysql.md": "0bde461414a0669797ff3c9c1c1ab6954dbb1bd6d1fd11a4d916fa8cb41ea683", + "package/orm.md": "2e622c77f4477c7e69f330b68b3253e3d54cf9ab4c124935269a677c31753919", + "package/orm-browser.md": "534cb7f207f14d425a3ccd03e2ad038137d3732ea1fd51a67019b315e02e766c", + "package/postgres.md": "9296b10815117f3f96af3171c36a554b8fb5033b4328d956338ce69adcb18d04", + "package/rpc.md": "c455f9ab0212e27b208b29948c9979feb9fd05143248474f91d5dce0b6c1d236", + "package/rpc-tcp.md": "4924f5e05bec8a288008315edef65bcc1ee5d940cc87b0126b82a9f188b78079", + "package/run.md": "16f7e3360ffcabe09bda174e6c8ed64055a869c41d2355c4d55344fde25ee411", + "package/sql.md": "8b4046cc206ff85449595ed7debce715e487097a3e9446f1841b7ef213c932dd", + "package/sqlite.md": "64fc4711e3557b9453300d2a99f01d69ea640d728483984ff35ea13142a79efb", + "package/stopwatch.md": "f8dc8d14b28ee81a00ac9c30f9278ce70ece11837dd132d47dfd0e70ed7fa8af", + "package/template.md": "135fe797c2a4309e2257bd118218927ce1b1d1265fa2297f5f02c75b4b62c5dc", + "package/topsort.md": "a0c2258b8859c5c7d864758f27699835d2017a630d3855eb08df4d4e36024def", + "package/type.md": "7d28ae8aca6dbbd0439bd7c778f5871d3ba7305660edeefa8fb7aa5ae80bfb3d", + "package/type-compiler.md": "819bcb710c7cc610238a69b37eb6809d693ccce84f3dc239642591d0cfcf9426", + "package/vite.md": "df1a5256d86a3560f3f0384122779255b9afe038c0c0eca4f705a23c336b51e2", + "package/workflow.md": "ebe03caf6abcd0ea512dd560ccf54c9fb34798b66c87780e6c24ce295cf8e76d" +} \ No newline at end of file diff --git a/website/src/translations/ja/basics.json b/website/src/translations/ja/basics.json new file mode 100644 index 000000000..10bf2605e --- /dev/null +++ b/website/src/translations/ja/basics.json @@ -0,0 +1,160 @@ +{ + "Deepkit is a modular framework for TypeScript backend web applications.": "Deepkit は TypeScript バックエンドの Web アプリケーション向けのモジュール式フレームワークです。", + "Structured, scalable, and built for enterprise-grade architecture.": "構造化され、スケーラブルで、エンタープライズ級のアーキテクチャのために構築されています。", + "Getting Started": "はじめに", + "View on GitHub": "GitHub で表示", + "Docs": "ドキュメント", + "Blog": "ブログ", + "Chapters": "章", + "Overview": "概要", + "Introduction": "導入", + "App": "アプリ", + "Getting started": "はじめに", + "Arguments & Flags": "引数とフラグ", + "Dependency Injection": "依存性注入", + "Modules": "モジュール", + "Services": "サービス", + "Events": "イベント", + "Logger": "ロガー", + "Configuration": "設定", + "Framework": "フレームワーク", + "Database": "データベース", + "Testing": "テスト", + "Deployment": "デプロイ", + "Public Assets": "公開アセット", + "Runtime Types": "ランタイム型", + "Type Annotations": "型注釈", + "Reflection": "リフレクション", + "Serialization": "シリアライゼーション", + "Validation": "バリデーション", + "Extend": "拡張", + "Custom serializer": "カスタムシリアライザー", + "External Types": "外部型", + "Bytecode": "バイトコード", + "Providers": "プロバイダー", + "Injection": "インジェクション", + "Scopes": "スコープ", + "Filesystem": "ファイルシステム", + "Local": "ローカル", + "Memory": "メモリ", + "AWS S3": "AWS S3", + "FTP": "FTP", + "sFTP (SSH)": "sFTP (SSH)", + "Google Storage": "Google Storage", + "Broker": "ブローカー", + "Cache": "キャッシュ", + "Message Bus": "メッセージバス", + "Message Queue": "メッセージキュー", + "Atomic Locks": "アトミックロック", + "Key Value": "キー・バリュー", + "HTTP": "HTTP", + "Input & Output": "入力と出力", + "Views": "ビュー", + "Middleware": "ミドルウェア", + "Security": "セキュリティ", + "RPC": "RPC", + "Errors": "エラー", + "Transport": "トランスポート", + "Database ORM": "データベース ORM", + "Entity": "エンティティ", + "Session": "セッション", + "Query": "クエリ", + "Transaction": "トランザクション", + "Inheritance": "継承", + "Relations": "リレーション", + "Migrations": "マイグレーション", + "ORM Browser": "ORM ブラウザー", + "Raw Access": "生のアクセス", + "Seeding": "シーディング", + "Composite primary key": "複合主キー", + "Soft-Delete": "ソフトデリート", + "Desktop UI": "デスクトップ UI", + "Styles": "スタイル", + "Adaptive Container": "アダプティブコンテナ", + "Button": "ボタン", + "Button group": "ボタングループ", + "Dialog": "ダイアログ", + "Drag": "ドラッグ", + "Dropdown": "ドロップダウン", + "Icons": "アイコン", + "Input": "入力", + "Menu": "メニュー", + "Slider": "スライダー", + "Radio": "ラジオ", + "Select": "セレクト", + "Checkbox": "チェックボックス", + "List": "リスト", + "Table": "テーブル", + "Tabs": "タブ", + "Window": "ウィンドウ", + "Window Toolbar": "ウィンドウツールバー", + "API": "API", + "Composition": "コンポジション", + "Command line interface (CLI) parser, config loader, dependency injection container, event system, modules system.": "コマンドラインインターフェイス (CLI) パーサー、設定ローダー、依存性注入コンテナ、イベントシステム、モジュールシステム。", + "App module that provides application/HTTP/RPC server, worker, debugger, integration tests.": "アプリケーション/HTTP/RPC サーバー、ワーカー、デバッガー、統合テストを提供するアプリモジュール。", + "App module that provides HTTP server based on Node http module with validation and serialization.": "検証とシリアライゼーションを備えた Node の http モジュールに基づく HTTP サーバーを提供するアプリモジュール。", + "Angular SSR": "Angular SSR", + "App module to integrate Angular SSR.": "Angular SSR を統合するためのアプリモジュール。", + "Infrastructure": "インフラストラクチャ", + "Remote procedure call (RPC) with binary encoding for WebSockets and TCP.": "WebSocket と TCP のためのバイナリエンコーディングを備えたリモートプロシージャコール (RPC)。", + "RPC TCP": "RPC TCP", + "TCP server and client for Deepkit RPC.": "Deepkit RPC 用の TCP サーバーとクライアント。", + "Message broker with queues, pub/sub, key-value, 2 level cache, and distributed locks.": "キュー、pub/sub、キー・バリュー、2 レベルキャッシュ、分散ロックを備えたメッセージブローカー。", + "Broker Redis": "Broker Redis", + "Broker Redis adapter.": "Broker Redis アダプター。", + "Unified API to work with local and remote filesystems.": "ローカルおよびリモートのファイルシステムを扱うための統一 API。", + "Filesystem FTP": "Filesystem FTP", + "Fileystem FTP adapter.": "Fileystem FTP アダプター。", + "Filesystem SFTP": "Filesystem SFTP", + "Fileystem SFTP (SSH) adapter.": "Fileystem SFTP (SSH) アダプター。", + "Filesystem S3": "Filesystem S3", + "Fileystem S3 adapter.": "Fileystem S3 アダプター。", + "Filesystem Google": "Filesystem Google", + "Fileystem Google Storage adapter.": "Fileystem Google Storage アダプター。", + "Filesystem Database": "Filesystem Database", + "Fileystem adapter for Deepkit ORM.": "Deepkit ORM 用の Fileystem アダプター。", + "ORM/DBAL": "ORM/DBAL", + "Object-relational Mapper (ORM) and data access library (DAL). MongoDB, SQLite, Postgres, MySQL.": "オブジェクト関係マッパー (ORM) とデータアクセスライブラリ (DAL)。MongoDB、SQLite、Postgres、MySQL。", + "ORM MySQL": "ORM MySQL", + "MySQL Adapter for Deepkit ORM.": "Deepkit ORM 用の MySQL アダプター。", + "ORM Postgres": "ORM Postgres", + "PostgreSQL Adapter for Deepkit ORM.": "Deepkit ORM 用の PostgreSQL アダプター。", + "ORM SQLite": "ORM SQLite", + "SQLite Adapter for Deepkit ORM.": "Deepkit ORM 用の SQLite アダプター。", + "ORM Mongo": "ORM Mongo", + "MongoDB Adapter for Deepkit ORM.": "Deepkit ORM 用の MongoDB アダプター。", + "Fundamentals": "基礎", + "Type": "型", + "Runtime types with reflection, JSON serialization, validation, and type guards.": "リフレクション、JSON シリアライゼーション、バリデーション、型ガードを備えたランタイム型。", + "Event": "イベント", + "Async and synchronous event dispatcher.": "非同期および同期のイベントディスパッチャー。", + "Dependency injection (DI) container with modules, config, scopes, and nominal type alias/interface support.": "モジュール、設定、スコープ、そして記名的な型エイリアス/インターフェイスのサポートを備えた依存性注入 (DI) コンテナ。", + "Template": "テンプレート", + "HTML template engine based on JSX.": "JSX ベースの HTML テンプレートエンジン。", + "Logger with scopes, colors, and custom transporter and formatter.": "スコープ、色、カスタムトランスポーターおよびフォーマッターを備えたロガー。", + "Workflow": "ワークフロー", + "Workflow engine / finite state machine.": "ワークフローエンジン / 有限状態機械。", + "Stopwatch": "ストップウォッチ", + "Profile and collect execution time of code.": "コードの実行時間をプロファイルし収集。", + "Tools": "ツール", + "Devtool": "Devtool", + "Chrome devtools for Deepkit (RPC).": "Deepkit (RPC) 用の Chrome DevTools。", + "Angular Desktop UI library.": "Angular デスクトップ UI ライブラリ。", + "Web user interface to manage ORM data.": "ORM データを管理するための Web ユーザーインターフェイス。", + "API console": "API コンソール", + "Tools to run benchmarks and collect statistics.": "ベンチマークを実行し統計を収集するためのツール。", + "Core": "コア", + "BSON": "BSON", + "BSON encoder and decoder.": "BSON エンコーダーおよびデコーダー。", + "Core functions for working with JavaScript.": "JavaScript を扱うためのコア関数。", + "Topsort": "トポロジカルソート", + "Topological sorting algorithm.": "トポロジカルソートアルゴリズム。", + "Runtime": "ランタイム", + "Vite": "Vite", + "Vite plugin to use Deepkit runtime types.": "Deepkit のランタイム型を利用するための Vite プラグイン。", + "Bun": "Bun", + "Bun plugin to use Deepkit runtime types.": "Deepkit のランタイム型を利用するための Bun プラグイン。", + "Type Compiler": "型コンパイラ", + "Type compiler as TypeScript plugin to make runtime types available.": "ランタイム型を利用可能にするための TypeScript プラグインとしての型コンパイラ。", + "Bench": "ベンチ" +} diff --git a/website/src/translations/ja/documentation/app.md b/website/src/translations/ja/documentation/app.md new file mode 100644 index 000000000..7759919cf --- /dev/null +++ b/website/src/translations/ja/documentation/app.md @@ -0,0 +1,181 @@ +# Deepkit App + +Deepkit App の抽象は Deepkit アプリケーションの最も基本的な構成要素です。ライブラリを単体で使用しない場合、通常はここからアプリケーションの構築を始めます。 +これは Node.js のようなランタイムで実行する通常の TypeScript ファイルです。アプリケーションのエントリポイントであり、CLI コマンド、サービス、 +設定、イベントなどを定義する方法を提供します。 + +Command-line Interface (CLI) プログラムは、テキスト入力とテキスト出力の形でターミナル経由で対話するプログラムです。この形態でアプリケーションと対話する利点は、 +ローカルまたは SSH 接続経由のいずれかでターミナルさえあればよい点です。 + +提供するもの: + +- CLI コマンド +- モジュールシステム +- サービスコンテナ +- 依存性注入 +- イベントシステム +- ロガー +- 設定ローダー(env, dotenv, json) + +Deepkit のコマンドは DI コンテナへ完全にアクセスできるため、すべてのプロバイダや設定オプションにアクセスできます。CLI コマンドの引数とオプションは +TypeScript の型によるパラメータ宣言で制御され、自動的にシリアライズおよび検証されます。 + +[Deepkit フレームワーク](./framework.md) の `@deepkit/framework` はこれをさらに拡張し、HTTP/RPC 用のアプリケーションサーバ、デバッガ/プロファイラなどを提供します。 + +## 簡単なインストール + +最も簡単な始め方は、NPM init を使って新しい Deepkit プロジェクトを作成することです。 + +```shell +npm init @deepkit/app@latest my-deepkit-app +```` + +これにより、新しいフォルダー `my-deepkit-app` が作成され、すべての依存関係と基本的な `app.ts` ファイルが含まれます。 + +```sh +cd my-deepkit-app +npm run app +```` + +これにより `ts-node` で `app.ts` ファイルが実行され、利用可能なコマンドが表示されます。ここから開始して、独自のコマンド、コントローラなどを追加できます。 + +## 手動インストール + +Deepkit App は [Deepkit ランタイム型](./runtime-types.md) を基盤としているため、すべての依存関係をインストールします: + +```bash +mkdir my-project && cd my-project + +npm install typescript ts-node +npm install @deepkit/app @deepkit/type @deepkit/type-compiler +``` + +次に、以下のコマンドを実行して、Deepkit の型コンパイラが `node_modules/typescript` にあるインストール済みの TypeScript パッケージへ導入されていることを確認します: + +```sh +./node_modules/.bin/deepkit-type-install +``` + +すべての peer 依存関係がインストールされていることを確認してください。デフォルトでは NPM 7+ が自動的にインストールします。 + +アプリケーションをコンパイルするには TypeScript コンパイラが必要で、アプリを容易に実行するために `ts-node` の使用を推奨します。 + +`ts-node` を使用しない代替としては、TypeScript コンパイラでソースコードをコンパイルし、生成された JavaScript を直接実行する方法があります。これは短いコマンドの +実行速度を劇的に向上させる利点があります。ただし、コンパイラを手動で実行するかウォッチャーを設定するかのいずれかで、ワークフローに追加のオーバーヘッドが生じます。 +このため、本ドキュメントのすべての例では `ts-node` を使用しています。 + +## 最初のアプリケーション + +Deepkit フレームワークは設定ファイルや特別なフォルダ構成を使用しないため、プロジェクトは自由に構成できます。開始するのに必要なのは、TypeScript の app.ts ファイルと +TypeScript の設定 tsconfig.json の2つだけです。 + +プロジェクトフォルダには次のファイルがある状態を目指します: + +``` +. +├── app.ts +├── node_modules +├── package-lock.json +└── tsconfig.json +``` + +基本的な tsconfig を設定し、`reflection` を `true` に設定して Deepkit の型コンパイラを有効にします。 +これは依存性注入コンテナやその他の機能を使用するために必要です。 + +```json title=tsconfig.json +{ + "compilerOptions": { + "outDir": "./dist", + "experimentalDecorators": true, + "strict": true, + "esModuleInterop": true, + "target": "es2020", + "module": "CommonJS", + "moduleResolution": "node" + }, + "reflection": true, + "files": [ + "app.ts" + ] +} +``` + +```typescript title=app.ts +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +const app = new App(); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +このコードでは、test コマンドを定義し、`run()` を使って直接実行する新しいアプリを作成しています。このスクリプトを実行するとアプリが起動します。 + +そしてそのまま実行します。 + +```sh +$ ./node_modules/.bin/ts-node app.ts +VERSION + Node + +USAGE + $ ts-node app.ts [COMMAND] + +TOPICS + debug + migration Executes pending migration files. Use migration:pending to see which are pending. + server Starts the HTTP server + +COMMANDS + test +``` + +次に、test コマンドを実行するには次のコマンドを実行します。 + +```sh +$ ./node_modules/.bin/ts-node app.ts test +Hello World +``` + +Deepkit では、以降の操作はすべてこの `app.ts` 経由で行います。ファイル名は自由に変更したり、複数作成したりできます。カスタム CLI コマンド、HTTP/RPC サーバ、 +マイグレーションコマンドなどはすべてこのエントリポイントから起動されます。 + +## 引数とフラグ + +Deepkit App は関数のパラメータを自動的に CLI の引数とフラグに変換します。パラメータの順序が CLI 引数の順序を決定します。 + +パラメータは任意の TypeScript の型にでき、自動的に検証およびデシリアライズされます。 + +詳しくは章 [引数とフラグ](./app/arguments.md) を参照してください。 + +## 依存性注入 + +Deepkit App はサービスコンテナを設定し、インポートされた各モジュールに対して親から継承する独自の依存性注入コンテナを用意します。 +これには標準で次のプロバイダが含まれており、サービス、コントローラ、イベントリスナに自動的に注入できます: + +- `Logger` ログ出力用 +- `EventDispatcher` イベント処理用 +- `CliControllerRegistry` 登録済み CLI コマンド用 +- `MiddlewareRegistry` 登録済みミドルウェア用 +- `InjectorContext` 現在のインジェクタコンテキスト用 + +Deepkit フレームワークをインポートすると、さらに多くのプロバイダが利用可能になります。詳細は [Deepkit フレームワーク](./framework.md) を参照してください。 + +## 終了コード + +終了コードはデフォルトで 0 で、これはコマンドが正常に実行されたことを意味します。終了コードを変更するには、execute メソッドまたはコマンドのコールバックで 0 以外の数値を返します。 + +```typescript + +@cli.controller('test') +export class TestCommand { + async execute() { + console.error('Error :('); + return 12; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/app/arguments.md b/website/src/translations/ja/documentation/app/arguments.md new file mode 100644 index 000000000..8e58fa13b --- /dev/null +++ b/website/src/translations/ja/documentation/app/arguments.md @@ -0,0 +1,318 @@ +# 引数とフラグ + +コマンドのターミナルでのコマンド引数は、`execute` メソッドや関数の通常の引数に相当します。これらは自動的にコマンドライン引数にマッピングされます。 +パラメータをオプションにすると、渡す必要はありません。デフォルト値がある場合も同様に必須ではありません。 + +型(string、number、union など)に応じて、渡された値は自動的にデシリアライズされ、検証されます。 + +```typescript +import { cli } from '@deepkit/app'; + +//関数スタイル +new App().command('test', (name: string) => { + console.log('Hello', name); +}); + +//クラス +@cli.controller('test') +class TestCommand { + async execute(name: string) { + console.log('Hello', name); + } +} +``` + +このコマンドを name パラメータを指定せずに実行すると、エラーが発生します: + +```sh +$ ts-node app.ts test +RequiredArgsError: Missing 1 required arg: +name +``` + +`--help` を使うと、必須引数に関する詳細情報が表示されます: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node-script app.ts test NAME +``` + +name を引数として渡すと、コマンドが実行され、name が正しく渡されます。 + +```sh +$ ts-node app.ts test "beautiful world" +Hello beautiful world +``` + +string、number、boolean、文字列リテラル、それらのユニオン、さらにそれらの配列といったあらゆるプリミティブなパラメータ型は、自動的に CLI 引数として扱われ、 +自動的に検証およびデシリアライズされます。パラメータの順序が CLI 引数の順序を決定します。パラメータはいくつでも追加できます。 + +インターフェース、クラス、オブジェクトリテラルのような複雑なオブジェクトが定義されると、それはサービスの依存関係として扱われ、 +依存性注入コンテナが解決を試みます。詳細は [依存性注入](dependency-injection.md) の章を参照してください。 + +## フラグ + +フラグは、コマンドに値を渡すもう一つの方法です。多くの場合オプションですが、必須にすることもできます。Type `Flag` で修飾されたパラメータは、`--name value` または `--name=value` で渡せます。 + +```typescript +import { Flag } from '@deepkit/app'; + +//関数スタイル +new App().command('test', (id: number & Flag) => { + console.log('id', name); +}); + +//クラス +class TestCommand { + async execute(id: number & Flag) { + console.log('id', id); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + --id=id (required) +``` + +ヘルプ表示の「OPTIONS」に、`--id` フラグが必要であることが示されています。このフラグを正しく入力すると、コマンドはその値を受け取ります。 + +```sh +$ ts-node app.ts test --id 23 +id 23 + +$ ts-node app.ts test --id=23 +id 23 +``` + +### ブール型のフラグ + +フラグには、特定の挙動を有効化するために値なしのフラグとして使える利点があります。パラメータがオプションの boolean としてマークされている場合、この挙動が有効になります。 + +```typescript +import { Flag } from '@deepkit/app'; + +//関数スタイル +new App().command('test', (remove: boolean & Flag = false) => { + console.log('delete?', remove); +}); + +//クラス +class TestCommand { + async execute(remove: boolean & Flag = false) { + console.log('delete?', remove); + } +} +``` + +```sh +$ ts-node app.ts test +delete? false + +$ ts-node app.ts test --remove +delete? true +``` + +### 複数の値を持つフラグ + +同じフラグに複数の値を渡すには、フラグを配列としてマークできます。 + +```typescript +import { Flag } from '@deepkit/app'; + +//関数スタイル +new App().command('test', (id: number[] & Flag = []) => { + console.log('ids', id); +}); + +//クラス +class TestCommand { + async execute(id: number[] & Flag = []) { + console.log('ids', id); + } +} +``` + +```sh +$ ts-node app.ts test +ids: [] + +$ ts-node app.ts test --id 12 +ids: [12] + +$ ts-node app.ts test --id 12 --id 23 +ids: [12, 23] +``` + +### 単一文字フラグ + +フラグを 1 文字でも渡せるようにするには、`Flag<{char: 'x'}>` を使用します。 + +```typescript +import { Flag } from '@deepkit/app'; + +//関数スタイル +new App().command('test', (output: string & Flag<{char: 'o'}>) => { + console.log('output: ', output); +}); + +//クラス +class TestCommand { + async execute(output: string & Flag<{char: 'o'}>) { + console.log('output: ', output); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + -o, --output=output (required) + + +$ ts-node app.ts test --output test.txt +output: test.txt + +$ ts-node app.ts test -o test.txt +output: test.txt +``` + +## オプション/デフォルト + +メソッド/関数のシグネチャは、どの引数やフラグがオプションかを定義します。型システム上でパラメータがオプションであれば、ユーザーはそれを指定する必要がありません。 + +```typescript + +//関数スタイル +new App().command('test', (name?: string) => { + console.log('Hello', name || 'nobody'); +}); + +//クラス +class TestCommand { + async execute(name?: string) { + console.log('Hello', name || 'nobody'); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +デフォルト値を持つパラメータについても同様です: + +```typescript +//関数スタイル +new App().command('test', (name: string = 'body') => { + console.log('Hello', name); +}); + +//クラス +class TestCommand { + async execute(name: string = 'body') { + console.log('Hello', name); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +これはフラグにも同様に適用されます。 + + +## シリアライズ/バリデーション + +すべての引数とフラグは、その型に基づいて自動的にデシリアライズおよび検証され、さらに追加の制約を与えることができます。 + +したがって、数値として定義された引数は、コマンドラインインターフェースがテキスト(つまり文字列)に基づいているにもかかわらず、コントローラー内では常に実数として保証されます。 + +```typescript +//関数スタイル +new App().command('test', (id: number) => { + console.log('id', id, typeof id); +}); + +//クラス +class TestCommand { + async execute(id: number) { + console.log('id', id, typeof id); + } +} +``` + +```sh +$ ts-node app.ts test 123 +id 123 number +``` + +追加の制約は、`@deepkit/type` の型注釈で定義できます。 + +```typescript +import { Positive } from '@deepkit/type'; +//関数スタイル +new App().command('test', (id: number & Positive) => { + console.log('id', id, typeof id); +}); + +//クラス +class TestCommand { + async execute(id: number & Positive) { + console.log('id', id, typeof id); + } +} +``` + +`id` の型 `Postive` は、正の数のみが許可されることを示します。ここでユーザーが負の数を渡すと、コードはまったく実行されず、エラーメッセージが表示されます。 + +```sh +$ ts-node app.ts test -123 +Validation error in id: Number needs to be positive [positive] +``` + +このような簡単に行える追加のバリデーションによって、コマンドは誤った入力に対してはるかに堅牢になります。詳細は [バリデーション](../runtime-types/validation.md) の章を参照してください。 + +## 説明 + +フラグや引数を説明するには、`@description` コメントデコレーターを使用します。 + +```typescript +import { Positive } from '@deepkit/type'; + +class TestCommand { + async execute( + /** @description ユーザーの識別子 */ + id: number & Positive, + /** @description ユーザーを削除しますか? */ + remove: boolean = false + ) { + console.log('id', id, typeof id); + } +} +``` + +ヘルプ表示では、この説明がフラグや引数の後に表示されます: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test ID + +ARGUMENTS + ID The users identifier + +OPTIONS + --remove Delete the user? +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/app/configuration.md b/website/src/translations/ja/documentation/app/configuration.md new file mode 100644 index 000000000..77ee169e3 --- /dev/null +++ b/website/src/translations/ja/documentation/app/configuration.md @@ -0,0 +1,264 @@ +# 設定 + +Deepkit アプリケーションでは、モジュールやアプリケーション自体に設定オプションを持たせることができます。たとえば、設定にはデータベースの URL、パスワード、IP などを含められます。サービス、HTTP/RPC/CLI コントローラ、テンプレート関数は依存性注入を通じてこれらの設定オプションを読み取れます。 + +設定は、プロパティを持つクラスを定義することで表現できます。これはアプリケーション全体の設定を型安全に定義する方法で、値は自動的にシリアライズおよび検証されます。 + +## 例 + +```typescript +import { MinLength } from '@deepkit/type'; +import { App } from '@deepkit/app'; + +class Config { + pageTitle: string & MinLength<2> = 'Cool site'; + domain: string = 'example.com'; + debug: boolean = false; +} + +const app = new App({ + config: Config +}); + + +app.command('print-config', (config: Config) => { + console.log('config', config); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Hello from Cool site via example.com +``` + +設定ローダーを使用しない場合は、既定値が使用されます。設定を変更するには、`app.configure({domain: 'localhost'})` メソッドを使うか、環境設定ローダーを使用します。 + +## 設定値の指定 + +デフォルトでは値は上書きされないため、既定値が使用されます。設定値を指定する方法はいくつかあります。 + +* `app.configure({})` 経由 +* 各オプション用の環境変数 +* JSON を用いた環境変数 +* dotenv ファイル + +複数の方法を同時に使って設定を読み込むことができます。呼び出し順が重要です。 + +### 環境変数 + +各設定オプションを個別の環境変数で設定できるようにするには、`loadConfigFromEnv` を使用します。既定のプレフィックスは `APP_` ですが、変更できます。`.env` ファイルも自動で読み込みます。デフォルトでは大文字の命名規則を使用しますが、これも変更できます。 + +上記の `pageTitle` のような設定オプションは、`APP_PAGE_TITLE="Other Title"` のようにして値を変更できます。 + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({prefix: 'APP_'}) + .run(); +``` + +```sh +APP_PAGE_TITLE="Other title" ts-node app.ts server:start +``` + +### JSON 環境変数 + +1 つの環境変数で複数の設定オプションを変更するには、`loadConfigFromEnvVariable` を使用します。最初の引数は環境変数名です。 + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnvVariable('APP_CONFIG') + .run(); +``` + +```sh +APP_CONFIG='{"pageTitle": "Other title"}' ts-node app.ts server:start +``` + +### DotEnv ファイル + +dotenv ファイルで複数の設定オプションを変更するには、`loadConfigFromEnv` を使用します。最初の引数は dotenv へのパス(`cwd` からの相対)または複数パスです。配列を渡した場合は、既存ファイルが見つかるまで各パスが順に試行されます。 + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({envFilePath: ['production.dotenv', 'dotenv']}) + .run(); +``` + +```sh +$ cat dotenv +APP_PAGE_TITLE=Other title +$ ts-node app.ts server:start +``` + +### モジュールの設定 + +各インポート済みモジュールにはモジュール名を持たせることができます。この名前は上記で使用した設定パスに用いられます。 + +たとえば環境変数で設定する場合、`FrameworkModule` のオプション port に対応するパスは `FRAMEWORK_PORT` です。すべての名前はデフォルトで大文字になります。プレフィックスに `APP_` を使う場合、次のようにしてポートを変更できます: + +```sh +$ APP_FRAMEWORK_PORT=9999 ts-node app.ts server:start +2021-06-12T18:59:26.363Z [LOG] Start HTTP server, using 1 workers. +2021-06-12T18:59:26.365Z [LOG] HTTP MyWebsite +2021-06-12T18:59:26.366Z [LOG] GET / helloWorld +2021-06-12T18:59:26.366Z [LOG] HTTP listening at http://localhost:9999/ +``` + +dotenv ファイルでも同様に `APP_FRAMEWORK_PORT=9999` となります。 + +一方、`loadConfigFromEnvVariable('APP_CONFIG')` を介した JSON 環境変数では、実際の設定クラスの構造になります。`framework` はオブジェクトになります。 + +```sh +$ APP_CONFIG='{"framework": {"port": 9999}}' ts-node app.ts server:start +``` + +これはすべてのモジュールで同様に動作します。アプリケーションの設定オプション(`new App`)にはモジュールのプレフィックスは不要です。 + + +## 設定クラス + +```typescript +import { MinLength } from '@deepkit/type'; + +export class Config { + title!: string & MinLength<2>; // 必須となり、値の指定が必要になります + host?: string; + + debug: boolean = false; // 既定値もサポートされています +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} +``` + +設定オプションの値は、モジュールのコンストラクタ、`.configure()` メソッド、または設定ローダー(例: 環境変数ローダー)で指定できます。 + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [new MyModule({title: 'Hello World'})], +}).run(); +``` + +インポートされたモジュールの設定オプションを動的に変更するには、`process` フックを使用できます。ここは、現在のモジュール設定や他のモジュールインスタンス情報に応じて設定オプションを差し替えたり、インポート済みモジュールをセットアップしたりするのに適した場所です。 + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} +``` + +アプリケーションレベルでは少し動作が異なります: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +ルートアプリケーションモジュールが通常のモジュールから作成される場合は、通常のモジュールと同様に動作します。 + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## 設定値を読み取る + +サービスで設定オプションを使用するには、通常の依存性注入を利用できます。設定オブジェクト全体、単一の値、または一部だけを注入することが可能です。 + +### 一部のみ + +設定値のサブセットだけを注入するには、`Pick` 型を使用します。 + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private config: Pick<Config, 'title' | 'host'}) { + } + + getTitle() { + return this.config.title; + } +} + + +// ユニットテストでは、次のようにインスタンス化できます +new MyService({title: 'Hello', host: '0.0.0.0'}); + +// あるいは型エイリアスを使用できます +type MyServiceConfig = Pick<Config, 'title' | 'host'}; +export class MyService { + constructor(private config: MyServiceConfig) { + } +} +``` + +### 単一値 + +単一の値だけを注入するには、インデックスアクセス演算子を使用します。 + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private title: Config['title']) { + } + + getTitle() { + return this.title; + } +} +``` + +### すべて + +すべての設定値を注入するには、クラスを依存関係として使用します。 + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private config: Config) { + } + + getTitle() { + return this.config.title; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/app/dependency-injection.md b/website/src/translations/ja/documentation/app/dependency-injection.md new file mode 100644 index 000000000..b9c3de113 --- /dev/null +++ b/website/src/translations/ja/documentation/app/dependency-injection.md @@ -0,0 +1,37 @@ +# 依存性注入 + +すべてのコマンドは依存性注入コンテナに完全にアクセスできます。コマンドやコントローラーのコンストラクターで依存関係を定義でき、依存性注入コンテナがそれを解決しようとします。 + +詳しくは [依存性注入](../dependency-injection.md) の章を参照してください。 + +```typescript +import { App, cli } from '@deepkit/app'; +import { Logger, ConsoleTransport } from '@deepkit/logger'; + +new App({ + providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport])}], +}).command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); +``` + +```typescript +@cli.controller('test', { + description: 'My super first command' +}) +class TestCommand { + constructor(protected logger: Logger) { + } + + async execute() { + this.logger.log('Hello World!'); + } +} + +new App({ + providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport]}], + controllers: [TestCommand] +}).run(); +``` + +必要なだけ多くの依存関係を定義できます。依存性注入コンテナがそれらを自動的に解決します。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/app/events.md b/website/src/translations/ja/documentation/app/events.md new file mode 100644 index 000000000..52f059c41 --- /dev/null +++ b/website/src/translations/ja/documentation/app/events.md @@ -0,0 +1,248 @@ +# イベントシステム + +イベントシステムは、同一プロセス内のアプリケーションコンポーネントがイベントの送信とリッスンによって通信できるようにします。これにより、互いを直接は認識していないかもしれない関数間でのメッセージ交換を容易にし、コードのモジュール化に役立ちます。 + +アプリケーションやライブラリは、処理の特定のポイントで追加の関数を実行する機会を提供します。これらの追加の関数は「イベントリスナー」として自身を登録します。 + +イベントはさまざまな形を取ります: + +- アプリケーションの起動またはシャットダウン。 +- 新しいユーザーの作成または削除。 +- エラーがスローされる。 +- 新しい HTTP リクエストを受信する。 + +Deepkit Framework と関連ライブラリは、ユーザーがリッスンして応答できるさまざまなイベントを提供します。さらに、必要に応じて好きなだけカスタムイベントを作成できる柔軟性もあり、アプリケーションをモジュール的に拡張できます。 + +## 使い方 + +Deepkit のアプリを使用している場合、イベントシステムはすでに含まれており、すぐに使用できます。 + +```typescript +import { App, onAppExecute } from '@deepkit/app'; + +const app = new App(); + +app.listen(onAppExecute, async (event) => { + console.log('MyEvent triggered!'); +}); + +app.run(); +``` + +イベントは `listen()` メソッドを使用するか、`@eventDispatcher.listen` デコレーターを使用するクラスによって登録できます: + +```typescript +import { App, onAppExecute } from '@deepkit/app'; +import { eventDispatcher } from '@deepkit/event'; + +class MyListener { + @eventDispatcher.listen(onAppExecute) + onMyEvent(event: typeof onAppExecute.event) { + console.log('MyEvent triggered!'); + } +} + +const app = new App({ + listeners: [MyListener], +}); +app.run(); +``` + +## イベントトークン + +Deepkit のイベントシステムの中心には Event Token があります。これは、イベント ID とイベントの型の両方を指定するユニークなオブジェクトです。イベントトークンは主に次の 2 つの目的を果たします。 + +- イベントをトリガーする役割。 +- トリガーしたイベントをリッスンする役割。 + +イベントトークンを使ってイベントが開始されると、そのトークンの所有者が事実上イベントの発生源として認識されます。トークンは、イベントに関連付けられたデータを決定し、非同期のイベントリスナーが利用できるかどうかを指定します。 + +```typescript +import { EventToken } from '@deepkit/event'; + +const MyEvent = new EventToken('my-event'); + +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); + +//app 参照経由でトリガー +await app.dispatch(MyEvent); + +//または EventDispatcher を使用。App の DI コンテナが自動的に注入します +app.command('test', async (dispatcher: EventDispatcher) => { + await dispatcher.dispatch(MyEvent); +}); +``` + +### カスタムイベントデータの作成: + +@deepkit/event の `DataEventToken` を使用する: + +```typescript +import { DataEventToken } from '@deepkit/event'; + +class User { +} + +const MyEvent = new DataEventToken<User>('my-event'); +``` + +BaseEvent を拡張する: + +```typescript +class MyEvent extends BaseEvent { + user: User = new User; +} + +const MyEventToken = new EventToken<MyEvent>('my-event'); +``` + +## 関数型リスナー + +関数型リスナーでは、シンプルな関数のコールバックをディスパッチャーに直接登録できます。方法は次のとおりです: + +```typescript +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +``` + +`logger: Logger` のような追加の引数を導入したい場合、Deepkit のランタイム型リフレクションにより、依存性注入システムによって自動的に注入されます。 + +```typescript +app.listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); +}); +``` + +最初の引数は必ずイベント自体でなければならない点に注意してください。この引数を省略することはできません。 + +`@deepkit/app` を使用している場合、app.listen() を使って関数型リスナーを登録することもできます。 + +```typescript +import { App } from '@deepkit/app'; + +new App() + .listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## クラスベースのリスナー + +クラスリスナーは、デコレーターを付与したクラスです。イベントをリッスンするための構造化された方法を提供します。 + +```typescript +import { App } from '@deepkit/app'; + +class MyListener { + @eventDispatcher.listen(UserAdded) + onUserAdded(event: typeof UserAdded.event) { + console.log('User added!', event.user.username); + } +} + +new App({ + listeners: [MyListener], +}).run(); +``` + +クラスリスナーでは、依存性注入はメソッドの引数またはコンストラクターを通して機能します。 + +## 依存性注入 + +Deepkit のイベントシステムは強力な依存性注入メカニズムを備えています。関数型リスナーを使用する場合、ランタイム型リフレクションシステムのおかげで、追加の引数が自動的に注入されます。同様に、クラスベースのリスナーもコンストラクターまたはメソッドの引数を通じて依存性注入をサポートします。 + +例えば、関数型リスナーの場合、`logger: Logger` のような引数を追加すると、関数が呼び出される際に適切な Logger インスタンスが自動的に提供されます。 + +```typescript +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +new App() + .listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## イベント伝播 + +すべてのイベントオブジェクトには stop() Function が備わっており、イベントの伝播を制御できます。イベントが停止されると、その後に追加された順序でのリスナーは実行されません。これは、特定の条件でイベント処理を停止する必要があるシナリオで特に有用で、イベントの実行と処理をきめ細かく制御できます。 + +例えば: + +```typescript +dispatcher.listen(MyEventToken, (event) => { + if (someCondition) { + event.stop(); + } + // さらなる処理 +}); +``` + +Deepkit フレームワークのイベントシステムにより、開発者はモジュール性が高く、スケーラブルで、メンテナブルなアプリケーションを容易に構築できます。イベントシステムを理解することで、特定の出来事や条件に基づいてアプリケーションの挙動を柔軟に調整できます。 + +## フレームワークのイベント + +Deepkit Framework 自体にも、アプリケーションサーバーから発火する複数のイベントがあり、リッスンできます。 + +_関数型リスナー_ + +```typescript +import { onServerMainBootstrap } from '@deepkit/framework'; +import { onAppExecute } from '@deepkit/app'; + +new App({ + imports: [new FrameworkModule] +}) + .listen(onAppExecute, (event) => { + console.log('Command about to execute'); + }) + .listen(onServerMainBootstrap, (event) => { + console.log('Server started'); + }) + .run(); +``` + +| 名前 | 説明 | +|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| onServerBootstrap | アプリケーションサーバーのブートストラップ時(メインプロセスおよびワーカー)に一度だけ呼び出されます。 | +| onServerBootstrapDone | アプリケーションサーバーが起動した直後に、ブートストラップ時(メインプロセスおよびワーカー)に一度だけ呼び出されます。 | +| onServerMainBootstrap | アプリケーションサーバーのブートストラップ時(メインプロセス)に一度だけ呼び出されます。 | +| onServerMainBootstrapDone | アプリケーションサーバーが起動した直後に、ブートストラップ時(メインプロセス)に一度だけ呼び出されます。 | +| onServerWorkerBootstrap | アプリケーションサーバーのブートストラップ時(ワーカープロセス)に一度だけ呼び出されます。 | +| onServerWorkerBootstrapDone | アプリケーションサーバーが起動した直後に、ブートストラップ時(ワーカープロセス)に一度だけ呼び出されます。 | +| onServerShutdownEvent | アプリケーションサーバーがシャットダウンするとき(マスタープロセスと各ワーカー)に呼び出されます。 | +| onServerMainShutdown | アプリケーションサーバーがメインプロセスでシャットダウンするときに呼び出されます。 | +| onServerWorkerShutdown | アプリケーションサーバーがワーカープロセスでシャットダウンするときに呼び出されます。 | +| onAppExecute | コマンドが実行されようとしているとき。 | +| onAppExecuted | コマンドが正常に実行されたとき。 | +| onAppError | コマンドの実行に失敗したとき。 | +| onAppShutdown | アプリケーションがシャットダウンしようとしているとき。 | + +## 低レベル API + +以下は @deepkit/event の低レベル API の例です。Deepkit App を使用する場合、イベントリスナーは EventDispatcher を通じて直接登録されるのではなく、モジュールを介して登録されます。とはいえ、必要であれば低レベル API も使用できます。 + +```typescript +import { EventDispatcher, EventToken } from '@deepkit/event'; + +//最初の引数には、依存性注入のために依存関係を解決する Injector コンテキストを渡せます +const dispatcher = new EventDispatcher(); +const MyEvent = new EventToken('my-event'); + +dispatcher.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +dispatcher.dispatch(MyEvent); + +``` + +### インストール + +イベントシステムは @deepkit/app に含まれています。スタンドアロンで使用したい場合は、手動でインストールできます: + +インストール手順は [イベント](../event.md) を参照してください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/app/logger.md b/website/src/translations/ja/documentation/app/logger.md new file mode 100644 index 000000000..0d4d6f11f --- /dev/null +++ b/website/src/translations/ja/documentation/app/logger.md @@ -0,0 +1,130 @@ +# ロガー + +Deepkit Logger は、情報のログ出力に使用できる主要な Logger クラスを備えたスタンドアロンのライブラリです。このクラスは、Deepkit アプリケーションの依存性注入コンテナで自動的に利用可能です。 + +`Logger` クラスにはいくつかのメソッドがあり、それぞれが `console.log` のように動作します。 + +| 名前 | ログレベル | レベル ID | +|------------------|-----------------------|-----------| +| logger.error() | エラー | 1 | +| logger.warning() | 警告 | 2 | +| logger.log() | デフォルトのログ | 3 | +| logger.info() | 特別な情報 | 4 | +| logger.debug() | デバッグ情報 | 5 | + + +既定では、ロガーのレベルは `info` で、info 以上のメッセージ(log、warning、error。ただし debug は含まない)のみを処理します。ログレベルを変更するには、例えば `logger.level = 5` を設定します。 + +## アプリケーションでの使用 + +Deepkit アプリケーションでロガーを使用するには、サービスやコントローラーに `Logger` をそのまま注入できます。 + +```typescript +import { Logger } from '@deepkit/logger'; +import { App } from '@deepkit/app'; + +const app = new App(); +app.command('test', (logger: Logger) => { + logger.log('This is a <yellow>log message</yellow>'); +}); + +app.run(); +``` + +## カラー + +ロガーはカラー付きのログメッセージをサポートします。色を付けたいテキストを囲む XML タグを使用してカラーを指定できます。 + +```typescript +const username = 'Peter'; +logger.log(`Hi <green>${username}</green>`); +``` + +カラーをサポートしないトランスポーターでは、色情報は自動的に取り除かれます。デフォルトのトランスポーター(`ConsoleTransport`)では色が表示されます。利用可能な色は次のとおりです: `black`, `red`, `green`, `blue`, `cyan`, `magenta`, `white`, `grey`/`gray`. + +## トランスポーター + +トランスポーターは 1 つでも複数でも設定できます。Deepkit アプリケーションでは、`ConsoleTransport` トランスポーターが自動的に設定されます。追加のトランスポーターを設定するには、[セットアップ呼び出し](dependency-injection.md#di-setup-calls)を使用します: + +```typescript +import { Logger, LoggerTransport } from '@deepkit/logger'; + +export class MyTransport implements LoggerTransport { + write(message: string, level: LoggerLevel, rawMessage: string) { + process.stdout.write(JSON.stringify({message: rawMessage, level, time: new Date}) + '\n'); + } + + supportsColor() { + return false; + } +} + +new App() + .setup((module, config) => { + module.configureProvider<Logger>(v => v.addTransport(new MyTransport)); + }) + .run(); +``` + +すべてのトランスポーターを新しいセットに置き換えるには、`setTransport` を使用します: + +```typescript +import { Logger } from '@deepkit/logger'; + +new App() +.setup((module, config) => { + module.configureProvider<Logger>(v => v.setTransport([new MyTransport])); +}) +.run(); +``` + +```typescript +import { Logger, JSONTransport } from '@deepkit/logger'; + +new App() + .setup((module, config) => { + module.configureProvider<Logger>(v => v.setTransport([new JSONTransport])); + }) + .run(); +``` + +## スコープ付きロガー + +スコープ付きロガーは各ログエントリに任意の領域名を追加します。これにより、アプリケーション内のどのサブ領域からログエントリが発生したかを判断するのに役立ちます。 + +```typescript +const scopedLogger = logger.scoped('database'); +scopedLogger.log('Query', query); +``` + +サービスにスコープ付きロガーを注入するために使用できる `ScopedLogger` 型もあります。 + +```typescript +import { ScopedLogger } from '@deepkit/logger'; + +class MyService { + constructor(protected logger: ScopedLogger) {} + doSomething() { + this.logger.log('This is wild'); + } +} +``` + +これで、スコープ付きロガーからのすべてのメッセージには `MyService` というスコープ名が前置されます。 + +## フォーマッター + +フォーマッターを使うと、メッセージ形式を変更できます。例えば、タイムスタンプを追加できます。アプリケーションが `server:start` で起動された場合、他にフォーマッターがないときは自動的に `DefaultFormatter` が追加されます(タイムスタンプ、範囲、ログレベルを追加します)。 + +## コンテキストデータ + +ログエントリにコンテキストデータを追加するには、最後の引数としてシンプルなオブジェクトリテラルを渡します。少なくとも 2 つの引数を持つログ呼び出しだけがコンテキストデータを含めることができます。 + +```typescript +const query = 'SELECT *'; +const user = new User; +logger.log('Query', {query, user}); // 最後の引数がコンテキストデータ +logger.log('Another', 'wild log entry', query, {user}); // 最後の引数がコンテキストデータ + +logger.log({query, user}); // これはコンテキストデータとしては扱われません。 +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/app/modules.md b/website/src/translations/ja/documentation/app/modules.md new file mode 100644 index 000000000..bbf632e9a --- /dev/null +++ b/website/src/translations/ja/documentation/app/modules.md @@ -0,0 +1,516 @@ +# モジュール + +Deepkit は高いモジュール性を持ち、アプリケーションを便利な複数のモジュールに分割できます。各モジュールは独自の依存性注入サブコンテナ(親の Provider をすべて継承)・設定・コマンドなどを持ちます。 +[はじめに](../framework.md) では、すでに 1 つのモジュール(ルートモジュール)を作成しました。`new App` はモジュールとほぼ同じ引数を取り、内部で自動的にルートモジュールを作成します。 + +アプリケーションをサブモジュールに分割する予定がない場合、またはモジュールを他者に提供するパッケージとして公開する予定がない場合は、この章はスキップできます。 + +モジュールはクラスモジュールとして、または関数型モジュールとして定義できます。 + +```typescript title=クラス モジュール +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + //new App({}) と同じオプション + providers: [MyService] +}) { +} +``` + +```typescript title=関数型モジュール +import { AppModule } from '@deepkit/app'; + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addProvider(MyService); + }; +} +``` + +このモジュールは、その後アプリケーションや他のモジュールにインポートできます。 + +```typescript +import { MyModule, myModule } from './module.ts' + +new App({ + imports: [ + new MyModule(), //クラス モジュールをインポート + myModule(), //関数型モジュールをインポート + ] +}).run(); +``` + +`App` と同様に、このモジュールに機能を追加できます。`createModule` の引数は同じですが、モジュール定義では imports は使用できません。 +関数型モジュールでは、`AppModule` のメソッドを使って、独自のオプションに基づいて動的に構成できます。 + +HTTP/RPC/CLI コントローラ、サービス、設定、イベントリスナー、各種モジュールフックを追加して、モジュールをより動的にします。 + +## コントローラ + +モジュールは、他のモジュールによって処理されるコントローラを定義できます。例えば、`@deepkit/http` パッケージのデコレータを持つコントローラを追加すると、その `HttpModule` がこれを検出し、見つかったルートをルーターに登録します。1 つのコントローラに複数のデコレータを含めることもできます。これらのデコレータを提供するモジュールの作者が、コントローラをどのように処理するかを決定します。 + +Deepkit には、そのようなコントローラを処理するパッケージが 3 つあります: HTTP、RPC、CLI。詳細はそれぞれの章を参照してください。以下は HTTP コントローラの例です: + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +class MyHttpController { + @http.GET('/hello) + hello() { + return 'Hello world!'; + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] +}) { +} + + +//App でも同様 +new App({ + controllers: [MyHttpController] +}).run(); +``` + +## Provider + +アプリケーションの `providers` セクションに Provider を定義すると、アプリ全体でアクセスできます。しかしモジュールの場合、これらの Provider はそのモジュールの依存性注入サブコンテナに自動的にカプセル化されます。別のモジュールやアプリケーションで利用できるようにするには、各 Provider を手動で export する必要があります。 + +Provider の仕組みの詳細は、[依存性注入](../dependency-injection.md) の章を参照してください。 + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +export class HelloWorldService { + helloWorld() { + return 'Hello there!'; + } +} + +class MyHttpController { + constructor(private helloService: HelloWorldService) { + } + + @http.GET('/hello) + hello() { + return this.helloService.helloWorld(); + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addController(MyHttpController); + module.addProvider(HelloWorldService); + }; +} + +//App でも同様 +new App({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}).run(); +``` + +ユーザーがこのモジュールをインポートしても、`HelloWorldService` にはアクセスできません。これは、`MyModule` のサブ依存性注入コンテナにカプセル化されているためです。 + +## エクスポート + +インポート側のモジュールで Provider を利用可能にするには、その Provider のトークンを `exports` に含めます。これは実質的に、その Provider を 1 つ上の親モジュール(インポート側)の依存性注入コンテナに移動します。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + exports: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addExport(HelloWorldService); + }; +} +``` + +`FactoryProvider` や `UseClassProvider` など他の Provider を使用している場合でも、exports にはクラスの型のみを指定してください。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] + providers: [ + { provide: HelloWorldService, useValue: new HelloWorldService } + ], + exports: [HelloWorldService], +}) { +} +``` + +これで、そのモジュールをインポートし、アプリケーションコードでエクスポートされたサービスを使用できます。 + +```typescript +import { App } from '@deepkit/app'; +import { cli, Command } from '@deepkit/app'; +import { HelloWorldService, MyModule } from './my-module'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected helloWorld: HelloWorldService) { + } + + async execute() { + this.helloWorld.helloWorld(); + } +} + +new App({ + controllers: [TestCommand], + imports: [ + new MyModule(), + ] +}).run(); +``` + +詳しくは [依存性注入](../dependency-injection.md) の章を参照してください。 + + +### 設定スキーマ + +モジュールには型安全な設定オプションを持たせることができます。これらの値は、そのモジュールのサービスにクラス参照や `Partial<Config, 'url'>` のような型関数を使って部分的または全体を注入できます。設定スキーマを定義するには、プロパティを持つクラスを記述します。 + +```typescript +export class Config { + title!: string; //必須で、値の提供が必要 + host?: string; //任意 + + debug: boolean = false; //デフォルト値もサポートされます +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.setConfigDefinition(Config).configure(options); + }; +} +``` + +設定オプションの値は、モジュールのコンストラクタ、`.configure()` メソッド、または設定ローダー(例: 環境変数ローダー)を通じて提供できます。 + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [ + new MyModule({title: 'Hello World'}), + myModule({title: 'Hello World'}), + ], +}).run(); +``` + +インポートしたモジュールの設定オプションを動的に変更するには、`process` モジュールフックを使用できます。これは、現在のモジュールの設定や他のモジュールインスタンス情報に応じて、インポートしたモジュールの設定を転送したりセットアップしたりするのに適した場所です。 + + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }; +} +``` + +アプリケーションレベルでは、少し異なります: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +ルートアプリケーションモジュールが通常のモジュールから作成されている場合は、通常のモジュールと同様に動作します。 + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## モジュール名 + +すべての設定オプションは、環境変数でも変更できます。これは、モジュールに名前が割り当てられている場合にのみ機能します。モジュール名は `createModule` で定義でき、インスタンス生成時に動的に変更することもできます。後者のパターンは、同じモジュールを 2 回インポートして新しい名前を設定して区別したい場合に便利です。 + +```typescript +export class MyModule extends createModuleClass({ + name: 'my' +}) { +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.name = 'my'; + }; +} +``` + +```typescript +import { MyModule } from './module'; + +new App({ + imports: [ + new MyModule(), //'my' がデフォルト名 + new MyModule().rename('my2'), //'my2' が新しい名前 + ] +}).run(); +``` + +環境変数や .env ファイルから設定オプションを読み込む方法の詳細は、[設定](./configuration.md) の章を参照してください。 + +## インポート + +モジュールは、機能を拡張するために他のモジュールをインポートできます。`App` では、モジュール定義オブジェクトの `imports: []` で他のモジュールをインポートできます: + +```typescript +new App({ + imports: [new Module] +}).run(); +``` + +通常のモジュールでは、これはできません。定義オブジェクト内でインスタンス化するとグローバルになってしまい、通常は望ましくないためです。代わりに、`imports` プロパティを介してモジュール自身の中でインスタンス化でき、モジュールの各新しいインスタンスごとに、各インポートモジュールのインスタンスが作成されます。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + imports = [new OtherModule()]; +} + +export function myModule() { + return (module: AppModule) => { + module.addImport(new OtherModule()); + }; +} +``` + +`process` フックを使用して、設定に基づいてモジュールを動的にインポートすることもできます。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + process() { + if (this.config.xEnabled) { + this.addImport(new OtherModule({ option: 'value' }); + } + } +} + +export function myModule(option: { xEnabled?: boolean } = {}) { + return (module: AppModule) => { + if (option.xEnabled) { + module.addImport(new OtherModule()); + } + }; +} +``` + +## フック + +サービスコンテナは、ルート/アプリケーションモジュールから開始して、インポートされた順にすべてのモジュールを読み込みます。 + +この過程で、サービスコンテナは登録されたすべての設定ローダーを実行し、`setupConfig` コールバックを呼び出し、各モジュールの設定オブジェクトを検証します。 + +サービスコンテナの読み込みプロセス全体は次のとおりです: + +1. 各モジュール `T`(ルートから開始)について + 1. 設定ローダー `ConfigLoader.load(T)` を実行。 + 2. `T.setupConfig()` を呼び出す。 + 3. `T` の設定を検証。無効なら中止。 + 4. `T.process()` を呼び出す。 + ここでモジュールは、有効な設定オプションに基づいて自分自身を変更できます。新しい import や provider などを追加します。 + 5. `T` の各インポートモジュールについて 1. を繰り返す。 +3. 登録されたすべてのモジュールを見つける。 +4. 見つかった各モジュール `T` を処理する。 + 1. `T` のミドルウェアを登録。 + 2. イベントディスパッチャに `T` のリスナーを登録。 + 3. 2. で見つかったすべてのモジュールに対して `Module.processController(T, controller)` を呼び出す。 + 4. 2. で見つかったすべてのモジュールに対して `Module.processProvider(T, token, provider)` を呼び出す。 + 5. `T` の各インポートモジュールについて 3. を繰り返す。 +5. すべてのモジュールで `T.postProcess()` を実行。 +6. すべてのモジュールでブートストラップクラスをインスタンス化。 +7. 依存性注入コンテナが構築される。 + +フックを使用するには、モジュールクラスに `process`、`processProvider`、`postProcess` メソッドを登録します。 + +```typescript +import { createModuleClass, AppModule } from '@deepkit/app'; +import { isClass } from '@deepkit/core'; +import { ProviderWithScope, Token } from '@deepkit/injector'; + +export class MyModule extends createModuleClass({}) { + imports = [new FrameworkModule()]; + + //最初に実行される + process() { + //this.config には完全に検証された設定オブジェクトが入っています。 + if (this.config.environment === 'development') { + this.getImportedModuleByClass(FrameworkModule).configure({ debug: true }); + } + this.addModule(new AnotherModule); + this.addProvider(Service); + + //追加のセットアップメソッドを呼び出します。 + //この例では、依存性注入コンテナが Service をインスタンス化する際に + //'method1' を指定の引数で呼び出します。 + this.configureProvider<Service>(v => v.method1(this.config.value)); + } + + //すべてのモジュールで見つかった各 Provider に対して実行される + processController(module: AppModule<any>, controller: ClassType) { + //例えば HttpModule は、各コントローラに @http デコレータが使われているか確認し、 + //使われていればすべてのルート情報を抽出してルーターに登録します。 + } + + //すべてのモジュールで見つかった各 Provider に対して実行される + processProvider(module: AppModule<any>, token: Token, provider: ProviderWithScope) { + //例えば FrameworkModule は、deepkit/orm の Database を拡張するトークンを探し、 + //それらを自動的に DatabaseRegistry に登録して、マイグレーション CLI コマンドや + //Framework Debugger で使用できるようにします。 + } + + //すべてのモジュールの処理が完了したときに実行される。 + //process/processProvider で処理した情報に基づいて、 + //module.configureProvider で Provider をセットアップする最後のチャンス。 + postProcess() { + + } +} +``` + +## 状態を持つモジュール + +各モジュールは `new Module` で明示的にインスタンス化されるため、モジュールは状態を持つことができます。この状態は依存性注入コンテナに注入でき、サービスで使用できるようになります。 + +例として HttpModule のユースケースを考えます。これはアプリケーション全体で登録された各コントローラをチェックし、@http デコレータが付いていればレジストリに登録します。このレジストリは Router に注入され、Router がインスタンス化されると、それらのコントローラのすべてのルート情報を抽出して登録します。 + +```typescript +class Registry { + protected controllers: { module: AppModule<any>, classType: ClassType }[] = []; + + register(module: AppModule<any>, controller: ClassType) { + this.controllers.push({ module, classType: controller }); + } + + get(classType: ClassType) { + const controller = this.controllers.find(v => v.classType === classType); + if (!controller) throw new Error('Controller unknown'); + return controller; + } +} + +class Router { + constructor( + protected injectorContext: InjectorContext, + protected registry: Registry + ) { + } + + getController(classType: ClassType) { + //指定の controller の classType に対応する classType と module を見つける + const controller = this.registry.get(classType); + + //ここでコントローラがインスタンス化されます。すでにインスタンス化されている場合は + //(provider が transient: true でない限り)以前のインスタンスが返されます + return injector.get(controller.classType, controller.module); + } +} + +class HttpModule extends createModuleClass({ + providers: [Router], + exports: [Router], +}) { + protected registry = new Registry; + + process() { + this.addProvider({ provide: Registry, useValue: this.registry }); + } + + processController(module: AppModule<any>, controller: ClassType) { + //コントローラは、コントローラの利用側によって module の providers に追加される必要があります + if (!module.isProvided(controller)) module.addProvider(controller); + this.registry.register(module, controller); + } +} + +class MyController {} + +const app = new App({ + controllers: [MyController], + imports: [new HttpModule()] +}); + +const myController = app.get(Router).getController(MyController); +``` + +## root 用 + +`root` プロパティを使うと、モジュールの依存性注入コンテナをルートアプリケーションのコンテナに移動できます。これにより、そのモジュールのすべてのサービスが、ルートアプリケーションから自動的に利用可能になります。基本的には、各 Provider(コントローラ、イベントリスナー、Provider)をルートコンテナに移動します。これは依存関係の競合を引き起こす可能性があるため、本当にグローバルなものしか持たないモジュールにのみ使用すべきです。代わりに、各 Provider を手動で export することを推奨します。 + +多くのモジュールで使用できるライブラリを構築する場合は、`root` の使用は避けるべきです。他のライブラリの Provider トークンと競合する可能性があるためです。例えば、このライブラリモジュールが `foo` モジュールをインポートし、そこで定義されたサービスを自分用に再構成し、ユーザーのアプリケーションも同じ `foo` モジュールをインポートする場合、ユーザーはあなたが再構成したサービスを受け取ることになります。多くの単純なユースケースでは問題ないかもしれませんが、注意が必要です。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + root = true; +} +``` + +サードパーティモジュールの `root` プロパティを `forRoot()` を使って変更することもできます。 + +```typescript +new App({ + imports: [new ThirdPartyModule().forRoot()], +}).run(); +``` + +## Injector Context + +InjectorContext は依存性注入コンテナです。自分のモジュールや他のモジュールからサービスを要求/インスタンス化できます。例えば、`processControllers` にコントローラを保存しておき、それらを正しくインスタンス化したい場合に必要になります。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/app/services.md b/website/src/translations/ja/documentation/app/services.md new file mode 100644 index 000000000..c223f6e09 --- /dev/null +++ b/website/src/translations/ja/documentation/app/services.md @@ -0,0 +1,62 @@ +# サービス + + +サービスは、アプリケーションに必要なあらゆる値、関数、または機能を包含する広いカテゴリです。サービスは通常、目的が狭く明確に定義されたクラスです。特定のことを行い、それをうまく実行すべきです。 + +Deepkit(およびほとんどの他の JavaScript/TypeScript フレームワーク)におけるサービスは、プロバイダを使ってモジュールに登録されるシンプルなクラスです。最も単純なプロバイダはクラスプロバイダで、クラス自体以外は何も指定しません。これにより、そのサービスは定義されたモジュールの依存性注入コンテナ内でシングルトンになります。 + +サービスは依存性注入コンテナによって管理・インスタンス化されるため、コンストラクタインジェクションまたはプロパティインジェクションを用いて、他のサービス、コントローラ、イベントリスナーでインポートして利用できます。詳細は[依存性注入](../dependency-injection)の章を参照してください。 + +単純なサービスを作成するには、目的を持ったクラスを作成します: + + +```typescript +export interface User { + username: string; +} + +export class UserManager { + users: User[] = []; + + addUser(user: User) { + this.users.push(user); + } +} +``` + +そして、それをアプリケーションまたはモジュールに登録します: + +```typescript +new App({ + providers: [UserManager] +}).run(); +``` + +これを行うと、このサービスをコントローラ、他のサービス、またはイベントリスナーで使用できます。たとえば、このサービスを CLI コマンドや HTTP ルートで使用してみましょう: + + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + providers: [UserManager], + imports: [new FrameworkModule({debug: true})] +}); + +app.command('test', (userManager: UserManager) => { + for (const user of userManager.users) { + console.log('User: ', user.username); + } +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', (userManager: UserManager) => { + return userManager.users; +}) + +app.run(); +``` + +サービスは Deepkit における基本的な構成要素であり、クラスに限定されません。実際、サービスはアプリケーションに必要な任意の値、関数、または機能であり得ます。詳しくは、[依存性注入](../dependency-injection)の章を参照してください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/broker.md b/website/src/translations/ja/documentation/broker.md new file mode 100644 index 000000000..197b3bdcd --- /dev/null +++ b/website/src/translations/ja/documentation/broker.md @@ -0,0 +1,155 @@ +# Deepkit Broker + +Deepkit Broker は、メッセージキュー、メッセージバス、イベントバス、Pub/Sub、キー/バリューストア、キャッシュ、そしてアトミック操作のための高性能な抽象化です。型安全性を重視し、自動シリアライゼーションとバリデーション、高性能、スケーラビリティを備えています。 + +Deepkit Broker はクライアントとサーバーを兼ね備えています。スタンドアロンのサーバーとして、または他の Deepkit Broker サーバーに接続するクライアントとして使用できます。マイクロサービスアーキテクチャ向けに設計されていますが、モノリシックアプリケーションでも使用できます。 + +クライアントはアダプタパターンを用いてさまざまなバックエンドをサポートします。異なるバックエンドでも同じコードを使え、同時に複数のバックエンドを使うこともできます。 + +現在、利用可能なアダプターは 3 つあります。1 つはデフォルトの `BrokerDeepkitAdapter` で、Deepkit Broker サーバーと通信し、Deepkit Broker(サーバーを含む)に同梱されています。 +2 つ目は [@deepkit/broker-redis](./package/broker-redis) にある `BrokerRedisAdapter` で、Redis サーバーと通信します。3 つ目はテスト目的のインメモリアダプターである `BrokerMemoryAdapter` です。 + +## インストール + +[Deepkit Framework](./framework.md) を使用する場合、Deepkit Broker はデフォルトでインストールおよび有効化されます。そうでない場合は、次のコマンドでインストールできます: + +```bash +npm install @deepkit/broker +``` + +## Broker クラス + +Deepkit Broker は次の主要なブローカークラスを提供します: + +- **BrokerCache** - キャッシュ無効化を備えた L2 キャッシュ抽象化 +- **BrokerBus** - メッセージバス (Pub/Sub) +- **BrokerQueue** - キューシステム +- **BrokerLock** - 分散ロック +- **BrokerKeyValue** - キー/バリューストア + +これらのクラスは、型安全な方法でブローカーサーバーと通信するためにブローカーアダプターを受け取るよう設計されています。 + +```typescript +import { BrokerBus, BrokerMemoryAdapter } from '@deepkit/broker'; + +const bus = new BrokerBus(new BrokerMemoryAdapter()); +const channel = bus.channel<{ foo: string }>('my-channel-name'); +await channel.subscribe((message) => { + console.log('received message', message); +}); + +await channel.publish({ foo: 'bar' }); +``` + +FrameworkModule はデフォルトアダプターを提供・登録し、デフォルトでは同一プロセスで動作する Deepkit Broker サーバーに接続します。 + +ランタイム型と `channel<Type>('my-channel-name');` という呼び出しのおかげで、すべてが型安全になり、メッセージはアダプター内で自動的にバリデーションとシリアライズを行えます。 +デフォルト実装の `BrokerDeepkitAdapter` はこれを自動で処理します(シリアライゼーションには BSON を使用)。 + +各ブローカークラスには独自のアダプターインターフェースがあるため、必要なメソッドのみを実装できます。`BrokerDeepkitAdapter` はこれらすべてのインターフェースを実装しており、すべてのブローカー API で使用できます。 + +## アプリケーションへの統合 + +これらのクラスを依存性注入とともに Deepkit アプリケーションで使用するには、次の機能を提供する `FrameworkModule` を使用します: + +- ブローカーサーバー用のデフォルトアダプター +- ブローカーサーバー本体(自動起動を含む) +- すべてのブローカークラスのプロバイダー登録 + +`FrameworkModule` は、指定された設定に基づいて構成済みのブローカーサーバー用デフォルトブローカーアダプターを提供します。 +また、すべてのブローカークラスのプロバイダーを登録するため、それら(例: BrokerBus)をサービスやプロバイダーファクトリに直接注入できます。 + +```typescript +// 別ファイル(例: broker-channels.ts) +type MyBusChannel = BrokerBusChannel<MyMessage>; + +const app = new App({ + providers: [ + Service, + provide<MyBusChannel>((bus: BrokerBus) => bus.channel<MyMessage>('my-channel-name')), + ], + imports: [new FrameworkModule({ + broker: { + // startOnBootstrap が true の場合、ブローカーサーバーはこのアドレスで起動します。Unix ソケットのパスまたは host:port の組み合わせ + listen: 'localhost:8811', // または 'var/broker.sock'; + // 別のブローカーサーバーを使用する場合は、そのアドレスを指定します。Unix ソケットのパスまたは host:port の組み合わせ。 + host: 'localhost:8811', // または 'var/broker.sock'; + // メインプロセスで単一のブローカーを自動的に起動します。カスタムのブローカーノードがある場合は無効にしてください。 + startOnBootstrap: true, + }, + })], +}); +``` + +その後、ブローカーの派生クラス(またはブローカークラス自体)をサービスに注入できます: + +```typescript +import { MyBusChannel } from './broker-channels.ts'; + +class Service { + constructor(private bus: MyBusChannel) { + } + + async addUser() { + await this.bus.publish({ foo: 'bar' }); + } +} +``` + +チャンネルの派生型(上の `MyBusChannel` のような)を作るのは常に良いアイデアです。これにより、サービスへ簡単に注入できます。 +そうしない場合、毎回 `BrokerBus` を注入して `channel<MyMessage>('my-channel-name')` を呼び出す必要があり、ミスを招きやすく DRY ではありません。 + +ほぼすべてのブローカークラスでこの種の派生が可能なため、1 か所で定義してどこでも使えます。詳細は該当するブローカークラスのドキュメントを参照してください。 + +## カスタムアダプター + +カスタムアダプターが必要な場合は、`@deepkit/broker` にある次のインターフェースのうち 1 つ以上を実装して独自のアダプターを作成できます: + +```typescript +export type BrokerAdapter = BrokerAdapterCache & BrokerAdapterBus & BrokerAdapterLock & BrokerAdapterQueue & BrokerAdapterKeyValue; +``` + +```typescript +import { BrokerAdapterBus, BrokerBus, Release } from '@deepkit/broker'; +import { Type } from '@deepkit/type'; + +class MyAdapter implements BrokerAdapterBus { + disconnect(): Promise<void> { + // 実装: ブローカーサーバーから切断する + } + async publish(name: string, message: any, type: Type): Promise<void> { + // 実装: ブローカーサーバーへメッセージを送信する。name は 'my-channel-name'、message は { foo: 'bar' } + } + async subscribe(name: string, callback: (message: any) => void, type: Type): Promise<Release> { + // 実装: ブローカーサーバーを購読する。name は 'my-channel-name' + } +} + +// もしくはデフォルトアダプターとして BrokerDeepkitAdapter を使用 +const adapter = new MyAdapter; +const bus = new BrokerBus(adapter); + +``` + +## ブローカーサーバー + +`FrameworkModule` をインポートして `server:start` コマンドを実行すると、デフォルトでブローカーサーバーが自動的に起動します。 +すべてのブローカークラスは、デフォルトでこのサーバーに接続するよう構成されています。 + +本番環境では、ブローカーサーバーを別プロセスまたは別マシンで実行します。 +`server:start` の代わりに `server:broker:start` でブローカーサーバーを起動します。 + +トラフィックが多くスケーラビリティが必要な場合は、代わりに [redis adapter](./package/broker-redis.md) を使用すべきです。Redis サーバーを実行する必要があるためセットアップはやや複雑ですが、より高性能で多くのトラフィックを処理できます。 + +```bash +ts-node app.ts server:broker:start +``` + +これは、例えば `new FrameworkModule({broker: {listen: 'localhost:8811'}})` で設定したホスト上でサーバーを起動します。 +`app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper'});` を有効にしていれば、環境変数でアドレスを変更することもできます: + +```bash +APP_FRAMEWORK_BROKER_LISTEN=localhost:8811 ts-node app.ts server:broker:start +``` + +サーバーを手動で起動する場合は、アプリケーション設定の `startOnBootstrap: false` によって自動ブローカーサーバー起動を無効化することを忘れないでください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/broker/atomic-locks.md b/website/src/translations/ja/documentation/broker/atomic-locks.md new file mode 100644 index 000000000..85302f395 --- /dev/null +++ b/website/src/translations/ja/documentation/broker/atomic-locks.md @@ -0,0 +1,75 @@ +# Broker のアトミックロック + +Deepkit Broker Locks は、複数のプロセスやマシンにまたがってアトミックロックを作成するためのシンプルな手段です。 + +あるコードブロックを一度に実行できるプロセスが1つだけであることを保証する簡単な方法です。 + +## 使い方 + +```typescript +import { BrokerLock } from '@deepkit/broker'; + +const lock = new BrokerLock(adapter); + +// ロックの有効期間は60秒です。 +// 取得のタイムアウトは10秒です。 +const myLock = lock.item('my-lock', { ttl: '60s', timeout: '10s' }); + +async function criticalSection() { + // 関数が完了するまでロックを保持します。 + // 関数がエラーを投げてもロックを自動的にクリーンアップします。 + await using hold = await lock1.hold(); +} +``` + +このロックは[明示的リソース管理](https://github.com/tc39/proposal-explicit-resource-management)をサポートしており、適切にロックを解放するために try-catch ブロックを使用する必要がありません。 + +ロックを手動で取得および解放するには、`acquire` と `release` メソッドを使用できます。 + +```typescript +// ロックが取得されるまでブロックします。 +// タイムアウトに達した場合は例外をスローします +await myLock.acquire(); + +try { + // クリティカルな処理を実行する +} finally { + await myLock.release(); +} +``` + +## アプリケーションでの使用 + +アプリケーションで BrokerLock を使用するための完全な例です。 +`FrameworkModule` をインポートすると、クラスは依存性注入コンテナで自動的に利用可能になります。 +詳細は「はじめに」ページを参照してください。 + +```typescript +import { BrokerLock, BrokerLockItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// この型は共有ファイルに移動してください +type MyCriticalLock = BrokerLockItem; + +class Service { + constructor(private criticalLock: MyCriticalLock) { + } + + async doSomethingCritical() { + await using hold = await this.criticalLock.hold(); + + // クリティカルな処理を実行する + // ロックは自動的に解放されます + } +} + +const app = new App({ + providers: [ + Service, + provide<MyCriticalLock>((lock: BrokerLock) => lock.item('my-critical-lock', { ttl: '60s', timeout: '10s' })), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/broker/cache.md b/website/src/translations/ja/documentation/broker/cache.md new file mode 100644 index 000000000..96dec62a2 --- /dev/null +++ b/website/src/translations/ja/documentation/broker/cache.md @@ -0,0 +1,91 @@ +# ブローカーキャッシュ + +Deepkit Broker Cache クラスは多層(2 層)のキャッシュで、メモリ内に揮発性のローカルキャッシュを保持し、データがキャッシュ内にない、古い、または無効化されている場合にのみブローカーサーバーからデータを取得します。これにより、非常に高いパフォーマンスと低レイテンシーのデータ取得のためにキャッシュを活用できます。 + +このキャッシュは型安全に設計されており、データを自動的にシリアライズ/デシリアライズします(BSON を使用)。また、キャッシュの無効化およびクリアをサポートします。実装では、同じキャッシュ項目に同時に複数のリクエストがアクセスしようとしても、プロセスごとにキャッシュが再構築されるのは一度だけであることを保証します。 + +データはサーバーに永続化されず、メモリ内にのみ保持されます。サーバーが再起動すると、すべてのデータは失われます。 + +## 使用方法 + +BrokerCache が依存性注入コンテナで利用できるようにアプリケーションを正しく設定する方法については、入門ページを必ずお読みください。 + +Deepkit Broker におけるキャッシュ抽象は単純なキー/バリュー ストアとは大きく異なります。キャッシュ名とビルダー関数を定義することで動作し、キャッシュが空または古い場合に自動的に呼び出されます。このビルダー関数は、キャッシュに保存されるデータを構築する役割を担います。 + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; + +const cache = new BrokerCache(adapter); + +const cacheItem = cache.item('my-cache', async () => { + // これは、キャッシュが空または古い場合に呼び出される + // ビルダー関数です + return 'hello world'; +}); + + +// キャッシュが古いか空かを確認 +await cacheItem.exists(); + +// キャッシュからデータを取得するか、ブローカーサーバーから取得します +// キャッシュが空または古い場合はビルダー関数が呼び出され +// その結果が返され、ブローカーサーバーへ送信されます。 +const topUsers = await cacheItem.get(); + +// キャッシュを無効化すると、次の get() 呼び出しで +// ビルダー関数が再度呼び出されます。 +// ローカルキャッシュとサーバーキャッシュをクリアします。 +await cacheItem.invalidate(); + +// キャッシュにデータを手動で設定 +await cacheItem.set(xy); +``` + +## アプリでの使用 + +アプリケーションで BrokerCache を使用するための完全な例。 +`FrameworkModule` をインポートすると、このクラスは依存性注入コンテナで自動的に利用可能になります。 +詳細は入門ページを参照してください。 + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// これらの型は共通ファイルで定義しておくと再利用でき、 +// サービスに注入することもできます +type MyCacheItem = BrokerCacheItem<User[]>; + +function createMyCache(cache: BrokerCache, database: Database) { + return cache.item<User[]>('top-users', async () => { + // これは、キャッシュが空または古い場合に呼び出される + // ビルダー関数です + return await database.query(User) + .limit(10).orderBy('score').find(); + }); +} + +class Service { + constructor(private cacheItem: MyCacheItem) { + } + + async getTopUsers() { + return await this.cacheItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + Database, + provide<MyCacheItem>(createMyCache), + ], + imports: [ + new FrameworkModule(), + ], +}); + +const cacheItem = app.get<MyCacheItem>(); + +// キャッシュからデータを取得するか、ブローカーサーバーから取得します +const topUsers = await cacheItem.get(); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/broker/key-value.md b/website/src/translations/ja/documentation/broker/key-value.md new file mode 100644 index 000000000..f43a110ef --- /dev/null +++ b/website/src/translations/ja/documentation/broker/key-value.md @@ -0,0 +1,90 @@ +# Broker Key-Value + +Deepkit Broker Key-Value の Class は、ブローカーサーバーと連携するシンプルな key/value ストア抽象化です。ブローカーサーバーからデータを保存および取得する簡単な方法を提供します。 + +ローカルキャッシュは実装されていません。すべての `get` 呼び出しは毎回ブローカーサーバーへの実際のネットワークリクエストです。これを避けるには、Broker Cache の抽象化を使用してください。 + +データはサーバー上で永続化されず、メモリ上にのみ保持されます。サーバーが再起動すると、すべてのデータは失われます。 + +## 使い方 + +```typescript +import { BrokerKeyValue } from '@deepkit/broker'; + +const keyValue = new BrokerKeyValue(adapter, { + ttl: '60s', // 各 key の有効期間。0 は ttl なし(デフォルト)を意味します。 +}); + +const item = keyValue.item<number>('key1'); + +await item.set(123); +console.log(await item.get()); //123 + +await item.remove(); +``` + +データは、指定された Type に基づき BSON を使用して自動的にシリアライズおよびデシリアライズされます。 + +Method `set` と `get` は `BrokerKeyValue` インスタンスで直接呼び出すこともできますが、その場合は毎回 key と Type を渡す必要があるという欠点があります。 + +```typescript +await keyValue.set<number>('key1', 123); +console.log(await keyValue.get<number>('key1')); //123 +``` + +## インクリメント + +`increment` Method は、指定した量で key の値をアトミックにインクリメントできます。 + +これはサーバー上に独自のストレージエントリを作成し、`set` や `get` とは互換性がないことに注意してください。 + +```typescript + +const activeUsers = keyValue.item<number>('activeUsers'); + +// 1 ずつアトミックにインクリメント +await activeUsers.increment(1); + +await activeUsers.increment(-1); + +// 現在の値を取得する唯一の方法は、0 を指定して increment を呼び出すことです +const current = await activeUsers.increment(0); + +// エントリを削除します +await activeUsers.remove(); +``` + +## アプリでの使用 + +アプリケーションで BrokerKeyValue を使用するための完全な例です。 +`FrameworkModule` をインポートすると、その Class は依存性注入コンテナで自動的に利用可能になります。 +詳しくは Getting started ページを参照してください。 + +```typescript +import { BrokerKeyValue, BrokerKeyValueItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// この Type は共有ファイルに移動してください +type MyKeyValueItem = BrokerKeyValueItem<User[]>; + +class Service { + constructor(private keyValueItem: MyKeyValueItem) { + } + + async getTopUsers(): Promise<User[]> { + // undefined の可能性があります。この場合を処理する必要があります。 + // これを避けたい場合は Broker Cache を使用してください。 + return await this.keyValueItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + provide<MyKeyValueItem>((keyValue: BrokerKeyValue) => keyValue.item<User[]>('top-users')), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/broker/message-bus.md b/website/src/translations/ja/documentation/broker/message-bus.md new file mode 100644 index 000000000..55f412b39 --- /dev/null +++ b/website/src/translations/ja/documentation/broker/message-bus.md @@ -0,0 +1,137 @@ +# ブローカーバス + +Deepkit メッセージバスは、アプリケーションの異なる部分間でメッセージやイベントを送受信できるメッセージバスシステム(Pub/Sub、分散イベントシステム)です。 + +マイクロサービス、モノリス、その他あらゆる種類のアプリケーションで使用できます。イベント駆動アーキテクチャに最適です。 + +これはプロセス内イベントに使用される Deepkit Event システムとは異なります。ブローカーバスは、他のプロセスやサーバーに送る必要があるイベントに使用します。ブローカーバスは、例えば `new FrameworkModule({workers: 4})` のように FrameworkModule によって自動的に起動された複数のワーカー間で通信したい場合にも最適です。 + +このシステムは型安全に設計されており、メッセージを自動的にシリアライズ/デシリアライズします(BSON を使用)。メッセージの型に[検証](../runtime-types/validation.md)を追加すると、送信前および受信後にもメッセージを検証します。これにより、メッセージが常に正しい形式で、想定どおりのデータを含むことが保証されます。 + +## 使用方法 + +```typescript +import { BrokerBus } from '@deepkit/broker'; + +const bus = new BrokerBus(adapter); + +// この型は共有ファイルに移動してください +type UserEvent = { type: 'user-created', id: number } | { type: 'user-deleted', id: number }; + +const channel = bus.channel<Events>('user-events'); + +await channel.subscribe((event) => { + if (event.type === 'user-created') { + console.log('User created', event.id); + } else if (event.type === 'user-deleted') { + console.log('User deleted', event.id); + } +}); + +await channel.publish({ type: 'user-created', id: 1 }); +``` + +チャネルに対して名前と型を定義することで、正しい型のメッセージだけが送受信されるようにできます。 +データは自動的にシリアライズおよびデシリアライズされます(BSON を使用)。 + +## アプリでの使用 + +アプリケーションで BrokerBus を使用する方法の完全な例です。 +`FrameworkModule` をインポートすると、このクラスは依存性注入コンテナで自動的に利用可能になります。 +詳細は「はじめに」ページを参照してください。 + +### サブジェクト + +デフォルトのメッセージ送受信方法は、rxjs の `Subject` 型を使用することです。その `subscribe` と `next` の各 Method により、型安全な方法でメッセージを送受信できます。すべての Subject インスタンスはブローカーによって管理され、Subject がガベージコレクションされると、サブスクリプションはブローカーのバックエンド(例: Redis)から削除されます。 + +メッセージの publish または subscribe に失敗した場合に対処するには、BrokerBus の `BusBrokerErrorHandler` をオーバーライドします。 + +このアプローチにより、業務コードをブローカーサーバーから適切に分離でき、ブローカーサーバーのないテスト環境でも同じコードを使用できます。 + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusSubject } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; +import { Subject } from 'rxjs'; + +// この型は共有ファイルに移動してください +type MyChannel = Subject<{ + id: number; + name: string; +}>; + +class Service { + // MyChannel はシングルトンではなく、各リクエストごとに新しいインスタンスが作成されます。 + // そのライフタイムはフレームワークによって監視され、サブジェクトがガベージコレクションされると、 + // サブスクリプションはブローカーのバックエンド(例: Redis)から削除されます。 + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }); + } + + update() { + this.channel.next({ id: 1, name: 'Peter' }); + } +} + +@rpc.controller('my-controller') +class MyRpcController { + constructor(private channel: MyChannel) { + } + + @rpc.action() + getChannelData(): MyChannel { + return this.channel; + } +} + +const app = new App({ + controllers: [MyRpcController], + providers: [ + Service, + provideBusSubject<MyChannel>('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` + +### バスチャネル + +メッセージが送信されたことの確認が必要で、各ケースでエラー処理を行いたい場合は、`BrokerBusChannel` 型を使用できます。`subscribe` と `publish` の各 Method は Promise を返します。 + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusChannel } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// この型は共有ファイルに移動してください +type MyChannel = BrokerBusChannel<{ + id: number; + name: string; +}>; + +class Service { + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }).catch(e => { + console.error('Error while subscribing', e); + }); + } + + async update() { + await this.channel.publish({ id: 1, name: 'Peter' }); + } +} + +const app = new App({ + providers: [ + Service, + provideBusChannel<MyChannel>('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/broker/message-queue.md b/website/src/translations/ja/documentation/broker/message-queue.md new file mode 100644 index 000000000..6df661a50 --- /dev/null +++ b/website/src/translations/ja/documentation/broker/message-queue.md @@ -0,0 +1,116 @@ +# ブローカーキュー + +Deepkit Message Queue は、メッセージをキューサーバーに送信し、ワーカーがそれらを処理できるメッセージキューシステムです。 + +このシステムは型安全に設計されており、メッセージを自動的にシリアライズ/デシリアライズします(BSON を使用)。 + +データはサーバーに永続化されるため、サーバーがクラッシュしてもデータは失われません。 + +## 使い方 + +```typescript +import { BrokerQueue, BrokerQueueChannel } from '@deepkit/broker'; + +const queue = new BrokerQueue(adapter); + +type User = { id: number, username: string }; + +const registrationChannel = queue.channel<User>('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +}); + +// ワーカーがメッセージを消費します。 +// これは通常、別プロセスで行われます。 +await registrationChannel.consume(async (user) => { + console.log('User registered', user); + // ここでワーカーがクラッシュしても、メッセージは失われません。 + // 別のワーカーに自動的に再配信されます。 + // このコールバックがエラーなく返ると、そのメッセージは + // 処理済みとしてマークされ、最終的に削除されます。 +}); + +// メッセージを送信するアプリケーション +await registrationChannel.produce({ id: 1, username: 'Peter' }); +``` + +## アプリでの使用 + +アプリケーションで BrokerQueue を使用する方法の完全な例です。 +`FrameworkModule` をインポートすると、そのクラスは依存性注入コンテナで自動的に利用可能になります。 +詳細は「はじめに」ページを参照してください。 + +キューシステムを最大限に活用するには、メッセージを消費する複数のワーカーを起動することを推奨します。 +HTTP ルートなどを持つメインのアプリケーションとは別の App を作成します。 + +共通のサービスは共有アプリモジュールを通じて共有します。チャネルの定義は、アプリケーション全体で共通のファイルを介して共有します。 + +```typescript +// ファイル: channels.ts + +export type RegistrationChannel = BrokerQueueChannel<User>; +export const registrationChannelProvider = provide<RegistrationChannel>((queue: BrokerQueue) => queue.channel<User>('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +})); +``` + +```typescript +// ファイル: worker.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +async function consumerCommand( + channel: RegistrationChannel, + database: Database) { + + await channel.consume(async (user) => { + // ユーザーに対して何らかの処理を行います。 + // 情報を保存したり、メールを送信したり、など。 + }); + + // ブローカーへの接続がプロセスを生かし続けます。 +} + +const app = new App({ + providers: [ + Database, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +app.command('consumer', consumerCommand); + +// 上のワーカーコマンドを直接起動 +void app.run('consumer'); +``` + +そしてアプリケーションでは次のようにメッセージを送信します: + +```typescript +// ファイル: app.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +class Service { + constructor(private channel: RegistrationChannel) { + } + + async registerUser(user: User) { + await this.channel.produce(user); + } +} + +const app = new App({ + providers: [ + Service, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +void app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/dependency-injection.md b/website/src/translations/ja/documentation/dependency-injection.md new file mode 100644 index 000000000..2b765b44c --- /dev/null +++ b/website/src/translations/ja/documentation/dependency-injection.md @@ -0,0 +1,197 @@ +# 依存性注入 + +Dependency Injection (DI) は、Class や Function が依存関係を「_受け取る_」設計パターンです。Inversion of Control (IoC) の原則に従い、複雑なコードをよりよく分離することで、テスタビリティ、モジュール性、明確性を大幅に向上させます。IoC の原則を適用するためのデザインパターンとしてはサービスロケーターパターンなどもありますが、特にエンタープライズソフトウェアにおいては、DI が支配的なパターンとして確立されています。 + +IoC の原則を説明するために、次の例を示します。 + +```typescript +import { HttpClient } from 'http-library'; + +class UserRepository { + async getUsers(): Promise<Users> { + const client = new HttpClient(); + return await client.get('/users'); + } +} +``` + +UserRepository Class は依存関係として HttpClient を持っています。この依存関係自体は特筆すべきものではありませんが、`UserRepository` が自分自身で HttpClient を生成していることが問題です。 +HttpClient の生成を UserRepository の中にカプセル化するのは良い考えのように思えるかもしれませんが、実はそうではありません。もし HttpClient を差し替えたい場合はどうでしょうか?実際の HTTP リクエストが外部に出ていかないようにして UserRepository をユニットテストしたい場合はどうでしょうか?この Class がそもそも HttpClient を使用していることを、どのようにして知るのでしょうか? + +## Inversion of Control + +Inversion of Control (IoC) の考え方では、HttpClient をコンストラクターで明示的な依存関係として受け取る(コンストラクターインジェクションとも呼ばれる)次のような別案があります。 + +```typescript +class UserRepository { + constructor( + private http: HttpClient + ) {} + + async getUsers(): Promise<Users> { + return await this.http.get('/users'); + } +} +``` + +これで UserRepository は HttpClient を生成する責任を持たず、UserRepository の利用者がその責任を負います。これが Inversion of Control (IoC) です。制御が反転(逆転)しました。具体的には、このコードは依存性注入を適用しています。依存関係は受け取られ(注入され)、もはや自分で生成したり要求したりしません。Dependency Injection は IoC の一つの変種にすぎません。 + +## Service Locator + +DI とは別に、Service Locator (SL) も IoC の原則を適用する方法です。これは依存関係を受け取るのではなく要求するため、一般的に Dependency Injection の対極と見なされます。もし上記コードで HttpClient を次のように要求した場合、これは Service Locator パターンと呼ばれます。 + +```typescript +class UserRepository { + async getUsers(): Promise<Users> { + const client = locator.getHttpClient(); + return await client.get('/users'); + } +} +``` + +`locator.getHttpClient` という Function は任意の名前を持てます。代替案としては、`useContext(HttpClient)`, `getHttpClient()`, `await import("client")` のような呼び出しや、`container.get(HttpClient)` や `container.http` のようなコンテナへの問い合わせがあります。グローバルの import はサービスロケーターの少し異なる変種で、モジュールシステム自体をロケーターとして使用します。 + +```typescript +import { httpClient } from 'clients' + +class UserRepository { + async getUsers(): Promise<Users> { + return await httpClient.get('/users'); + } +} +``` + +これらの変種に共通しているのは、HttpClient という依存関係を明示的に要求し、コードがサービスコンテナの存在を知っているという点です。これはコードをフレームワークに強く結びつけ、コードをクリーンに保つためには避けたいものです。 + +サービスの要求は、Property のデフォルト値としてだけでなく、コードの途中のどこかでも発生しえます。コードの途中ということは Type の Interface の一部ではないため、HttpClient の使用が隠蔽されます。HttpClient の要求方法のバリエーションによっては、別の実装に置き換えることが非常に難しかったり、完全に不可能な場合もあります。特にユニットテストの領域や明確さのために、ここで困難が生じることがあり、そのためサービスロケーターは状況によってはアンチパターンと分類されます。 + +## Dependency Injection + +Dependency Injection では、何かを要求するのではなく、利用者によって明示的に提供されるか、コードが受け取ります。利用側はサービスコンテナにアクセスせず、`HttpClient` がどのように生成または取得されるかを知りません。本質的には、IoC フレームワークからコードを疎結合にし、よりクリーンにします。 + +必要なのは `HttpClient` が Type として必要だと宣言することだけです。Service Locator に対する Dependency Injection の大きな差異であり利点の一つは、Dependency Injection を使用するコードが、あらゆる種類のサービスコンテナやサービス識別システムなしでも問題なく動作することです(サービスに名前を付ける必要がありません)。これは単なる Type の宣言であり、IoC フレームワークの文脈外でも機能します。 + +前の例で見たように、すでに依存性注入パターンが適用されています。具体的にはコンストラクターインジェクションが見られ、依存関係がコンストラクターで宣言されています。したがって、UserRepository は次のようにインスタンス化する必要があります。 + +```typescript +const users = new UserRepository(new HttpClient()); +``` + +UserRepository を使用したいコードは、その依存関係すべてを提供(注入)しなければなりません。HttpClient を毎回生成するべきか、毎回同じものを使うべきかは、Class 自身ではなく、その Class の利用者が決めます。もはや(Class の観点では)サービスロケーターの場合のように要求されることも、最初の例のように完全に自分で生成されることもありません。このフローの反転にはさまざまな利点があります。 + +* すべての依存関係が明示的に見えるため、コードが理解しやすくなる。 +* すべての依存関係が一意で、必要に応じて簡単に差し替えられるため、テストが容易になる。 +* 依存関係を簡単に交換できるため、コードがよりモジュール化される。 +* UserRepository が非常に複雑な依存関係を自ら生成する責任を負わなくなるため、関心の分離 (Separation of Concern) の原則を促進する。 + +しかし、明らかな欠点もすぐに見えてきます。HttpClient のようなすべての依存関係を本当に自分で作成または管理する必要があるのでしょうか?答えは Yes と No です。Yes、多くのケースでは依存関係を自分で管理するのは完全に正当です。良い API の証は、依存関係が手に負えなくならないこと、そしてそうなったとしても使い心地が良いことです。多くのアプリケーションや複雑なライブラリでは、それが当てはまる場合があります。多くの依存関係を持つ非常に複雑な低レベル API を、利用者にとって簡素化して提供するには、Facade パターンが素晴らしく有効です。 + +## 依存性注入コンテナ + +しかし、より複雑なアプリケーションでは、すべての依存関係を自分で管理する必要はありません。まさにそのために、いわゆる依存性注入コンテナが存在します。これはすべてのオブジェクトを自動的に生成するだけでなく、依存関係も自動的に「注入」するため、手動での "new" 呼び出しが不要になります。コンストラクターインジェクション、メソッドインジェクション、プロパティインジェクションなど、さまざまな種類のインジェクションがあります。これにより、多数の依存関係を持つ複雑なアーキテクチャでも容易に管理できます。 + +依存性注入コンテナ(DI コンテナまたは IoC コンテナとも呼ばれる)は、Deepkit では `@deepkit/injector` として提供されており、Deepkit Framework では App モジュールを通じてすでに統合されています。上記のコードは、`@deepkit/injector` パッケージの低レベル API を使用すると次のようになります。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders( + [UserRepository, HttpClient] +); + +const userRepo = injector.get(UserRepository); + +const users = await userRepo.getUsers(); +``` + +この場合の `injector` オブジェクトが依存性注入コンテナです。"new UserRepository" を使う代わりに、コンテナは `get(UserRepository)` を用いて UserRepository のインスタンスを返します。コンテナを静的に初期化するために、`InjectorContext.forProviders` Function にプロバイダーの一覧(この場合は単に Class)を渡します。 +DI は依存関係を提供することがすべてなので、コンテナには依存関係が提供されます。これが「プロバイダー」という技術用語の由来です。 + +プロバイダーには複数の種類があります: ClassProvider, ValueProvider, ExistingProvider, FactoryProvider。これらを組み合わせることで、DI コンテナで非常に柔軟なアーキテクチャを表現できます。 + +プロバイダー間のすべての依存関係は自動的に解決され、`injector.get()` 呼び出しが発生した時点でオブジェクトと依存関係が生成・キャッシュされ、コンストラクター引数として正しく渡され(これがコンストラクターインジェクション)、Property として設定され(これがプロパティインジェクション)、またはメソッド呼び出しに渡されます(これがメソッドインジェクション)。 + +さて、HttpClient を別のものに交換するには、HttpClient に対して別のプロバイダー(ここでは ValueProvider)を定義できます。 + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useValue: new AnotherHttpClient()}, +]); +``` + +`injector.get(UserRepository)` を通じて UserRepository が要求されると、AnotherHttpClient オブジェクトを受け取ります。代わりに ClassProvider を使うのも非常に有効で、AnotherHttpClient の依存関係も DI コンテナによって管理されます。 + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useClass: AnotherHttpClient}, +]); +``` + +プロバイダーのすべての種類は、[依存性注入のプロバイダー](./dependency-injection/providers.md) セクションで一覧と説明があります。 + +ここで言及しておくべきは、Deepkit の DI コンテナは Deepkit のランタイム Type にのみ対応しているということです。つまり、Class、Type、Interface、Function を含むコードは、ランタイムで Type 情報を利用できるようにするために、Deepkit Type Compiler でコンパイルされる必要があります。詳しくは [ランタイム型](./runtime-types.md) の章を参照してください。 + +## 依存性逆転 + +先ほどの UserRepository の例では、UserRepository が下位レイヤーの HTTP ライブラリに依存しています。さらに、抽象 (Interface) ではなく具体的な実装 (Class) を依存関係として宣言しています。一見するとオブジェクト指向のパラダイムに沿っているように見えますが、特に複雑で大規模なアーキテクチャでは問題につながる可能性があります。 + +別の案としては、HttpClient の依存関係を抽象 (Interface) に変換し、UserRepository に HTTP ライブラリのコードを import しないようにすることが考えられます。 + +```typescript +interface HttpClientInterface { + get(path: string): Promise<any>; +} + +class UserRepository { + concstructor( + private http: HttpClientInterface + ) {} + + async getUsers(): Promise<Users> { + return await this.http.get('/users'); + } +} +``` + +これは依存性逆転の原則と呼ばれます。UserRepository はもはや HTTP ライブラリに直接依存せず、抽象(Interface)に基づきます。これにより、この原則の二つの基本的な目標を満たします。 + +* 高水準モジュールは低水準モジュールから何も import すべきではない。 +* 実装は抽象(Interface)に基づくべきである。 + +二つの実装(HTTP ライブラリを用いた UserRepository)の統合は、DI コンテナを通じて行えます。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); +``` + +Deepkit の DI コンテナは HttpClientInterface のような抽象的な依存関係(Interface)を解決できるため、HttpClient が HttpClientInterface を実装している限り、UserRepository は自動的に HttpClient の実装を取得します。 + +これは、HttpClient が明示的に HttpClientInterface を実装する(`class HttpClient implements HttpClientInterface`)か、あるいは HttpClient の API が単に HttpClientInterface と互換であることによって実現されます。 + +HttpClient が API を変更(たとえば `get` Method を削除)して HttpClientInterface と互換でなくなった時点で、DI コンテナは Error を投げます("the HttpClientInterface dependency was not provided")。ここでは、両方の実装を結びつけたい利用者が解決策を見つける責任を負います。例としては、HttpClientInterface を実装し、Method 呼び出しを正しく HttpClient に中継するアダプター Class を登録することが挙げられます。 + +代替として、HttpClientInterface に具体的な実装を直接提供することもできます。 + +```typescript +import { InjectorContext, provide } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository, HttpClientInterface } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + provide<HttpClientInterface>({useClass: HttpClient}), +]); +``` + +ここで注意すべきは、理論的には依存性逆転の原則に利点がある一方で、実践では大きな欠点もあるということです。より多くの Interface を記述しなければならないためコード量が増えるだけでなく、各実装が依存関係ごとに Interface を持つことになるため、複雑性も増します。このコストを支払う価値があるのは、アプリケーションが一定の規模に達し、この柔軟性が必要になったときだけです。あらゆるデザインパターンや原則には費用対効果があり、適用する前に熟考すべきです。 + +デザインパターンは、最も単純なコードにまで闇雲に一律適用すべきではありません。しかし、複雑なアーキテクチャ、大規模アプリケーション、拡大するチームといった前提条件が揃っているならば、依存性逆転やその他のデザインパターンこそが真価を発揮します。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/dependency-injection/configuration.md b/website/src/translations/ja/documentation/dependency-injection/configuration.md new file mode 100644 index 000000000..113d143e6 --- /dev/null +++ b/website/src/translations/ja/documentation/dependency-injection/configuration.md @@ -0,0 +1,92 @@ +# 設定 + +依存性注入コンテナは、設定オプションの注入も可能です。設定の注入は、コンストラクタ注入またはプロパティ注入で受け取ることができます。 + +Module API は、設定定義(通常のクラス)の宣言をサポートしています。このクラスにプロパティを用意すると、各プロパティが設定オプションとして機能します。TypeScript におけるクラスの定義方法により、各プロパティごとに型やデフォルト値を定義できます。 + +```typescript +class RootConfiguration { + domain: string = 'localhost'; + debug: boolean = false; +} + +const rootModule = new InjectorModule([UserRepository]) + .setConfigDefinition(RootConfiguration) + .addImport(lowLevelModule); +``` + +設定オプション `domain` と `debug` は、プロバイダ内で型安全に便利に使用できます。 + +```typescript +class UserRepository { + constructor(private debug: RootConfiguration['debug']) {} + + getUsers() { + if (this.debug) console.debug('fetching users ...'); + } +} +``` + +オプションの値は `configure()` で設定できます。 + +```typescript + rootModule.configure({debug: true}); +``` + +デフォルト値がないが必須なオプションは `!` を付けて指定できます。これにより、モジュールの利用者は値を必ず提供しなければならず、そうでない場合はエラーになります。 + +```typescript +class RootConfiguration { + domain!: string; +} +``` + +## 検証 + +また、前章の[バリデーション](../runtime-types/validation.md)と[シリアライズ](../runtime-types/serialization.md)にあるすべてのシリアライズおよびバリデーションの型を使用して、オプションが満たすべき型および内容の制約を詳細に指定できます。 + +```typescript +class RootConfiguration { + domain!: string & MinLength<4>; +} +``` + +## 注入 + +設定オプションは、他の依存関係と同様に、先に示したように DI コンテナを通じて安全かつ容易に注入できます。最も簡単な方法は、インデックスアクセス演算子を使って単一のオプションを参照することです。 + +```typescript +class WebsiteController { + constructor(private debug: RootConfiguration['debug']) {} + + home() { + if (this.debug) console.debug('visit home page'); + } +} +``` + +設定オプションは個別だけでなくグループとしても参照できます。この目的には TypeScript のユーティリティ型 `Partial` を使用します。 + +```typescript +class WebsiteController { + constructor(private options: Partial<RootConfiguration, 'debug' | 'domain'>) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +すべての設定オプションを取得するには、設定クラス自体を直接参照することもできます。 + +```typescript +class WebsiteController { + constructor(private options: RootConfiguration) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +ただし、実際に使用する設定オプションだけを参照することを推奨します。これはユニットテストを単純化するだけでなく、コードから実際に必要なものを把握しやすくします。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/dependency-injection/getting-started.md b/website/src/translations/ja/documentation/dependency-injection/getting-started.md new file mode 100644 index 000000000..cc636ba42 --- /dev/null +++ b/website/src/translations/ja/documentation/dependency-injection/getting-started.md @@ -0,0 +1,110 @@ +# はじめに + +Deepkit における Dependency Injection はランタイムタイプに基づいているため、ランタイムタイプが正しくインストールされている必要があります。 [ランタイムタイプ](../runtime-types/getting-started.md) を参照してください。 + +これが成功していれば、`@deepkit/injector` をインストールするか、すでに内部でこのライブラリを使用している Deepkit フレームワークを使うことができます。 + +```sh + npm install @deepkit/injector +``` + +ライブラリがインストールされると、その API を直接使用できます。 + + +## 使い方 + +Dependency Injection を使用する方法は 3 つあります。 + +* Injector API(低レベル) +* Module API +* App API(Deepkit フレームワーク) + +Deepkit フレームワークなしで `@deepkit/injector` を使用する場合は、最初の 2 つの方法を推奨します。 + +### Injector API + +Injector API はすでに [Dependency Injection の紹介](../dependency-injection) で説明しました。これは、単一の DI コンテナを作成する単一のクラス `InjectorContext` によって非常にシンプルに使用でき、モジュールを持たないシンプルなアプリケーションに特に適しています。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); + +const repository = injector.get(UserRepository); +``` + +この場合の `injector` オブジェクトは Dependency Injection コンテナです。`InjectorContext.forProviders` 関数はプロバイダーの配列を受け取ります。どの値を渡せるかについては、[Dependency Injection のプロバイダー](dependency-injection.md#di-providers) の章を参照してください。 + +### Module API + +より複雑な API は `InjectorModule` クラスで、プロバイダーを複数のモジュールに分けて保持し、モジュールごとにカプセル化された複数の DI コンテナを作成できます。さらに、モジュールごとに設定クラスを使用でき、プロバイダーに渡す設定値を自動検証したうえで提供しやすくなります。モジュールは相互にインポートでき、プロバイダーをエクスポートして、階層を構築し、きれいに分離されたアーキテクチャを実現できます。 + +アプリケーションがより複雑で、Deepkit フレームワークを使用しない場合は、この API を使用すべきです。 + +```typescript +import { InjectorModule, InjectorContext } from '@deepkit/injector'; + +const lowLevelModule = new InjectorModule([HttpClient]) + .addExport(HttpClient); + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +const injector = new InjectorContext(rootModule); +``` + +この場合の `injector` オブジェクトは Dependency Injection コンテナです。プロバイダーは複数のモジュールに分割でき、モジュールの import を使って別の場所から再度インポートできます。これにより、アプリケーションやアーキテクチャの階層を反映した自然な階層が構築されます。 +InjectorContext には常に階層の最上位のモジュール(root モジュールまたは app モジュール)を渡すべきです。InjectorContext は仲介の役割のみを持ち、`injector.get()` への呼び出しは単に root モジュールへ転送されます。ただし、第二引数としてモジュールを渡すことで、非 root モジュールからプロバイダーを取得することも可能です。 + +```typescript +const repository = injector.get(UserRepository); + +const httpClient = injector.get(HttpClient, lowLevelModule); +``` + +すべての非 root モジュールはデフォルトでカプセル化されており、そのモジュール内のすべてのプロバイダーはそのモジュール自身からのみ利用可能です。プロバイダーを他のモジュールから利用可能にするには、そのプロバイダーをエクスポートする必要があります。エクスポートにより、そのプロバイダーは階層の親モジュールへと移動し、その形で利用できるようになります。 + +すべてのプロバイダーをデフォルトで最上位(root モジュール)へエクスポートするには、`forRoot` オプションを使用できます。これにより、すべてのプロバイダーを他のすべてのモジュールから使用できるようになります。 + +```typescript +const lowLevelModule = new InjectorModule([HttpClient]) + .forRoot(); // すべてのプロバイダーを root にエクスポート +``` + +### App API + +Deepkit フレームワークを使用する場合、モジュールは `@deepkit/app` API で定義します。これは Module API を基盤としており、その機能も利用できます。さらに、強力なフックを用いて処理したり、より動的なアーキテクチャを実現するための設定ローダーを定義したりできます。 + +[フレームワークのモジュール](../app/modules.md) の章で詳しく説明しています。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry, HttpBody } from '@deepkit/http'; + +interface User { + username: string; +} + +class Service { + users: User[] = []; +} + +const app = new App({ + providers: [Service], + imports: [new FrameworkModule()], +}); + +const router = app.get(HttpRouterRegistry); + +router.post('/users', (body: HttpBody<User>, service: Service) => { + service.users.push(body); +}); + +router.get('/users', (service: Service): Users => { + return service.users; +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/dependency-injection/injection.md b/website/src/translations/ja/documentation/dependency-injection/injection.md new file mode 100644 index 000000000..22173ac05 --- /dev/null +++ b/website/src/translations/ja/documentation/dependency-injection/injection.md @@ -0,0 +1,70 @@ +# インジェクション + +依存関係が注入されるため、Dependency Injection と呼ばれます。インジェクションは、ユーザー(手動)または DI コンテナ(自動)によって行われます。 + +## コンストラクタ インジェクション + +ほとんどの場合、コンストラクタ インジェクションが使われます。すべての依存関係はコンストラクタの引数として指定され、DI コンテナによって自動的に注入されます。 + +```typescript +class MyService { + constructor(protected database: Database) { + } +} +``` + +オプションの依存関係はそのように明示する必要があります。そうしないと、プロバイダが見つからない場合に Error が発生する可能性があります。 + +```typescript +class MyService { + constructor(protected database?: Database) { + } +} +``` + +## プロパティ インジェクション + +コンストラクタ インジェクションの代替として、プロパティ インジェクションがあります。これは通常、依存関係がオプションである場合や、コンストラクタがそれ以上引数でいっぱいな場合に使用されます。インスタンスが生成され(つまりコンストラクタが実行され)ると、プロパティは自動的に代入されます。 + +```typescript +import { Inject } from '@deepkit/core'; + +class MyService { + // 必須 + protected database!: Inject<Database>; + + // またはオプション + protected database?: Inject<Database>; +} +``` + +## パラメータ インジェクション + +さまざまな場所でコールバック関数を定義できます。例えば HTTP ルートや CLI コマンドです。この場合、依存関係をパラメータとして定義できます。DI コンテナによって自動的に注入されます。 + +```typescript +import { Database } from './db'; + +app.get('/', (database: Database) => { + //... +}); +``` + +## Injector コンテキスト + +依存関係を動的に解決したい場合は、`InjectorContext` をインジェクトして、それを使って依存関係を取得できます。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class MyService { + constructor(protected context: InjectorContext) { + } + + getDatabase(): Database { + return this.context.get(Database); + } +} +``` + +これは [Dependency Injection のスコープ](./scopes.md) を扱う際に特に有用です。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/dependency-injection/providers.md b/website/src/translations/ja/documentation/dependency-injection/providers.md new file mode 100644 index 000000000..2153aa5c1 --- /dev/null +++ b/website/src/translations/ja/documentation/dependency-injection/providers.md @@ -0,0 +1,312 @@ +# プロバイダ + +Dependency Injection コンテナに依存関係を提供する方法はいくつかあります。最も単純な方法は、クラスを指定するだけです。これは短縮形の ClassProvider とも呼ばれます。 + +```typescript +new App({ + providers: [UserRepository] +}); +``` + +これはクラスのみを指定する特別なプロバイダを表します。その他のプロバイダはすべてオブジェクトリテラルとして指定する必要があります。 + +デフォルトでは、すべてのプロバイダはシングルトンとしてマークされ、常に 1 つのインスタンスのみが存在します。プロバイダの解決ごとに新しいインスタンスを作成したい場合は、`transient` オプションを使用します。これにより、クラスは毎回再生成され、またはファクトリは毎回実行されます。 + +```typescript +new App({ + providers: [{ provide: UserRepository, transient: true }] +}); +``` + +## ClassProvider + +短縮形の ClassProvider のほかに、通常の ClassProvider もあり、こちらはクラスではなくオブジェクトリテラルです。 + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: UserRepository }] +}); +``` + +これは次の 2 つと同等です: + +```typescript +new App({ + providers: [{ provide: UserRepository }] +}); + +new App({ + providers: [UserRepository] +}); +``` + +これを使って、あるプロバイダを別のクラスに置き換えることができます。 + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: OtherUserRepository }] +}); +``` + +この例では、`OtherUserRepository` クラスも DI コンテナで管理され、すべての依存関係が自動的に解決されます。 + +## ValueProvider + +静的な値をこのプロバイダで提供できます。 + +```typescript +new App({ + providers: [{ provide: OtherUserRepository, useValue: new OtherUserRepository() }] +}); +``` + +依存関係として提供できるのはクラスインスタンスだけではないため、`useValue` には任意の値を指定できます。プロバイダトークンとしてシンボルやプリミティブ(string、number、boolean)を使用することもできます。 + +```typescript +new App({ + providers: [{ provide: 'domain', useValue: 'localhost' }] +}); +``` + +プリミティブなプロバイダトークンは、依存関係として Inject 型で宣言する必要があります。 + +```typescript +import { Inject } from '@deepkit/core'; + +class EmailService { + constructor(public domain: Inject<string, 'domain'>) {} +} +``` + +inject エイリアスとプリミティブなプロバイダトークンの組み合わせは、実行時の型情報を含まないパッケージから依存関係を提供する場合にも使用できます。 + +```typescript +import { Inject } from '@deepkit/core'; +import { Stripe } from 'stripe'; + +export type StripeService = Inject<Stripe, '_stripe'>; + +new App({ + providers: [{ provide: '_stripe', useValue: new Stripe }] +}); +``` + +そして、利用側では次のように宣言します: + +```typescript +class PaymentService { + constructor(public stripe: StripeService) {} +} +``` + +## ExistingProvider + +既に定義済みのプロバイダへのフォワーディングを定義できます。 + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useValue: new OtherUserRepository()}, + {provide: UserRepository, useExisting: OtherUserRepository} + ] +}); +``` + +## FactoryProvider + +関数を使用してプロバイダに値を提供できます。この関数はパラメータを取ることもでき、そのパラメータは DI コンテナから提供されます。これにより、他の依存関係や設定オプションにアクセスできます。 + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: () => { + return new OtherUserRepository() + }}, + ] +}); + +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: (domain: RootConfiguration['domain']) => { + return new OtherUserRepository(domain); + }}, + ] +}); + +new App({ + providers: [ + Database, + {provide: OtherUserRepository, useFactory: (database: Database) => { + return new OtherUserRepository(database); + }}, + ] +}); +``` + +## InterfaceProvider + +クラスやプリミティブに加えて、抽象(インターフェース)も提供できます。これは `provide` 関数を介して行い、提供する値が型情報を含まない場合に特に有用です。 + +```typescript +import { provide } from '@deepkit/injector'; + +interface Connection { + write(data: Uint16Array): void; +} + +class Server { + constructor (public connection: Connection) {} +} + +class MyConnection { + write(data: Uint16Array): void {} +} + +new App({ + providers: [ + Server, + provide<Connection>(MyConnection) + ] +}); +``` + +複数のプロバイダが Connection インターフェースを実装している場合、最後のプロバイダが使用されます。 + +provide() の引数としては、他のすべての種類のプロバイダを指定できます。 + +```typescript +const myConnection = {write: (data: any) => undefined}; + +new App({ + providers: [ + provide<Connection>({ useValue: myConnection }) + ] +}); + +new App({ + providers: [ + provide<Connection>({ useFactory: () => myConnection }) + ] +}); +``` + +## 非同期プロバイダ + +`@deepkit/injector` の設計上、非同期の Dependency Injection コンテナにおける非同期プロバイダの使用は想定されていません。これは、プロバイダの要求も非同期である必要があり、アプリケーション全体を最上位レベルで非同期として動作させる必要があるためです。 + +何かを非同期に初期化する必要がある場合、その初期化はアプリケーションサーバーのブートストラップに移すべきです。そこではイベントを非同期にできます。あるいは、初期化を手動でトリガーすることもできます。 + +## プロバイダの構成 + +構成コールバックを使用すると、プロバイダの結果を操作できます。これは、別の依存性注入の手法であるメソッドインジェクションを用いる場合などに便利です。 + +これらは module API または app API でのみ使用でき、モジュールの上位で登録されます。 + +```typescript +class UserRepository { + private db?: Database; + setDatabase(db: Database) { + this.db = db; + } +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider<UserRepository>(v => { + v.setDatabase(db); +}); +``` + +`configureProvider` はコールバックの第 1 引数 `v` として UserRepository のインスタンスを受け取り、そのメソッドを呼び出せます。 + +メソッド呼び出しに加えて、プロパティの設定も可能です。 + +```typescript +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider<UserRepository>(v => { + v.db = new Database(); +}); +``` + +すべてのコールバックはキューに格納され、定義された順序で実行されます。 + +キュー内の呼び出しは、プロバイダが作成され次第、その実際の結果に対して実行されます。つまり、ClassProvider ではインスタンス生成直後のクラスインスタンスに適用され、FactoryProvider ではファクトリの戻り値に適用され、ValueProvider ではその値自体に適用されます。 + +静的な値だけでなく他のプロバイダも参照するために、コールバックの引数として定義するだけで任意の依存関係を注入できます。これらの依存関係がプロバイダのスコープで解決可能であることを確認してください。 + +```typescript +class Database {} + +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository, Database]) +rootModule.configureProvider<UserRepository>((v, db: Database) => { + v.db = db; +}); +``` + +## 名目型 + +`configureProvider` に渡される型(前の例の `UserRepository` など)は、構造的型付けではなく名目型によって解決されることに注意してください。これは、同じ構造でも異なるアイデンティティを持つ 2 つのクラス/インターフェースは互換ではないことを意味します。これは `get<T>` の呼び出しや依存関係の解決時にも同様です。 + +これは構造的型付けに基づく TypeScript の型チェックの方式とは異なります。この設計判断は、偶発的な誤設定(例: 任意のクラスと構造的に互換な空のクラスを要求してしまう等)を避け、コードをより堅牢にするためのものです。 + + +次の例では `User1` と `User2` クラスは構造的には互換ですが、名目的には互換ではありません。つまり、`User1` を要求しても `User2` は解決されず、その逆も同様です。 + +```typescript + +class User1 { + name: string = ''; +} + +class User2 { + name: string = ''; +} + +new App({ + providers: [User1, User2] +}); +``` + +クラスの継承やインターフェースの実装は、名目的な関係を確立します。 + +```typescript +class UserBase { + name: string = ''; +} + +class User extends UserBase { +} + +const app = new App({ + providers: [User2] +}); + +app.get(UserBase); // User を返す +``` + +```typescript +interface UserInterface { + name: string; +} + +class User implements UserInterface { + name: string = ''; +} + +const app = new App({ + providers: [User] +}); + +app.get<UserInterface>(); // User を返す +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/dependency-injection/scopes.md b/website/src/translations/ja/documentation/dependency-injection/scopes.md new file mode 100644 index 000000000..bbbc5e7a3 --- /dev/null +++ b/website/src/translations/ja/documentation/dependency-injection/scopes.md @@ -0,0 +1,54 @@ +# スコープ + +デフォルトでは、DI コンテナのすべてのプロバイダーはシングルトンであり、一度だけインスタンス化されます。つまり、UserRepository の例では、実行全体を通して常に UserRepository のインスタンスは 1 つだけです。ユーザーが "new" キーワードで手動で作成しない限り、2 つ目のインスタンスが作成されることはありません。 + +しかし、プロバイダーを短時間だけ、または特定のイベント中だけインスタンス化すべきさまざまなユースケースがあります。そのようなイベントは、たとえば HTTP リクエストや RPC 呼び出しです。これは、各イベントごとに新しいインスタンスが作成され、そのインスタンスが使用されなくなった後は自動的に(ガーベジコレクタによって)除去されることを意味します。 + +HTTP リクエストはスコープの典型的な例です。たとえば、セッション、ユーザーオブジェクト、その他のリクエスト関連のプロバイダーなどをこのスコープに登録できます。スコープを作成するには、任意のスコープ名を選び、プロバイダーにそのスコープを指定します。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class UserSession {} + +const injector = InjectorContext.forProviders([ + {provide: UserSession, scope: 'http'} +]); +``` + +スコープを指定すると、そのプロバイダーは DI コンテナから直接取得できなくなるため、次の呼び出しは失敗します。 + +```typescript +const session = injector.get(UserSession); //例外をスロー +``` + +代わりに、スコープ付きの DI コンテナを作成する必要があります。これは HTTP リクエストが来るたびに行われます。 + +```typescript +const httpScope = injector.createChildScope('http'); +``` + +このスコープに登録されたプロバイダーだけでなく、スコープを定義していないすべてのプロバイダーも、このスコープ付き DI コンテナから取得できます。 + +```typescript +const session = httpScope.get(UserSession); //動作する +``` + +すべてのプロバイダーはデフォルトでシングルトンであるため、各スコープ付きコンテナ内では `get(UserSession)` を呼び出すたびに常に同じインスタンスが返されます。複数のスコープ付きコンテナを作成すると、複数の UserSession が作成されます。 + +スコープ付き DI コンテナは、外部から動的に値を設定できます。たとえば、HTTP スコープでは HttpRequest と HttpResponse オブジェクトを簡単にセットできます。 + +```typescript +const injector = InjectorContext.forProviders([ + {provide: HttpResponse, scope: 'http'}, + {provide: HttpRequest, scope: 'http'}, +]); + +httpServer.on('request', (req, res) => { + const httpScope = injector.createChildScope('http'); + httpScope.set(HttpRequest, req); + httpScope.set(HttpResponse, res); +}); +``` + +Deepkit フレームワークを使用するアプリケーションには、デフォルトで `http`、`rpc`、`cli` のスコープがあります。詳しくはそれぞれ [CLI](../cli.md)、[HTTP](../http.md)、[RPC](../rpc.md) の章を参照してください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem.md b/website/src/translations/ja/documentation/filesystem.md new file mode 100644 index 000000000..e07091e5f --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem.md @@ -0,0 +1,273 @@ +# Deepkit Filesystem + +Deepkit Filesystem は、ローカルおよびリモートのファイルシステム向けのファイルシステム抽象化レイヤーです。ファイルがローカルに保存されているかリモートサーバーに保存されているかに関係なく、ファイルやディレクトリを統一的な方法で扱えるようにします。 + +標準でサポートされているファイルシステム: + +- ローカルファイルシステム(`@deepkit/filesystem` に同梱) +- メモリ(`@deepkit/filesystem` に同梱) +- FTP(`@deepkit/filesystem-ftp` に同梱) +- SFTP(`@deepkit/filesystem-sftp` に同梱) +- AWS S3(`@deepkit/filesystem-aws-s3` に同梱) +- Google Cloud Filesystem(`@deepkit/filesystem-google` に同梱) + +## インストール + +```bash +npm install @deepkit/filesystem +``` + +注意: NPM を使用しない場合は、peer dependencies が正しくインストールされていることを確認してください。 + +## 使い方 + +```typescript +import { Filesystem, FilesystemLocalAdapter } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter('/path/to/my/files'); +const filesystem = new Filesystem(adapter); + +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} + +await filesystem.write('myFile.txt', 'Hello World'); +const file = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); + +const content = await filesystem.read('myFile.txt'); +``` + + +## ファイル一覧 + +ファイルを一覧するには `files()` Method を使用します。`File` オブジェクトの配列を返します。 + +```typescript +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} +``` + +配列が空の場合、フォルダが存在しないか、中にファイルがありません。 + +すべてのファイルを再帰的に一覧するには `allFiles()` Method を使用します。 + +```typescript +const files = await filesystem.allFiles(); +for (const file of files) { + console.log(file.path); +} +``` + +## ファイルの読み取り + +ファイルを読み取るには `read()` Method を使用します。ファイル内容を `Uint8Array` として返します。 + +```typescript +const content = await filesystem.read('myFile.txt'); +``` + +テキストとして読み取るには `readAsText()` Method を使用します。文字列を返します。 + +```typescript +const content = await filesystem.readAsText('myFile.txt'); +``` + +## ファイルの書き込み + +ファイルを書き込むには `write()` Method を使用します。`Uint8Array` または文字列を受け付けます。 + +```typescript +await filesystem.write('myFile.txt', 'Hello World'); +``` + +ローカルファイルから書き込むには `writeFile()` Method を使用します。 + +最初の引数はファイル名ではなくディレクトリであることに注意してください。ファイル名は、指定したファイルのベース名です。 +ファイル名は第2引数に {name: 'myFile.txt'} オブジェクトを渡すことで変更できます。拡張子が指定されていない場合は、 +拡張子は自動的に検出されます。 + +```typescript +const path = await filesystem.writeFile('/', { path: '/path/to/local/file.txt' }); + +// UploadedFile に対応 +router.post('/upload', async (body: HttpBody<{ file: UploadedFile }>, filesystem: Filesystem, session: Session) => { + const user = session.getUser(); + const path = await filesystem.writeFile('/user-images', body.file, { name: `user-${user.id}` }); + //path = /user-images/user-123.jpg +}); +``` + + +## ファイルの削除 + +ファイルを削除するには `delete()` Method を使用します。 + +```typescript +await filesystem.delete('myFile.txt'); +``` + +これはルートフォルダ内の `myFile.txt` を削除します。パスがディレクトリの場合は Error をスローします。 + +## ディレクトリの削除 + +ディレクトリを削除するには `deleteDirectory()` Method を使用します。ディレクトリと、その中のすべてのファイルおよびディレクトリを削除します。 + +```typescript +await filesystem.deleteDirectory('myFolder'); +``` + +## ディレクトリの作成 + +ディレクトリを作成するには `createDirectory()` Method を使用します。 + +```typescript +await filesystem.makeDirectory('myFolder'); +``` + +## ファイルの移動 + +ファイルまたはディレクトリを移動するには `move()` Method を使用します。`File` オブジェクトまたはパスを受け付けます。 + +```typescript +await filesystem.move('myFile.txt', 'myFolder/myFile.txt'); +``` + +`myFolder` ディレクトリが存在しない場合は作成されます。 + +## ファイルのコピー + +ファイルまたはディレクトリをコピーするには `copy()` Method を使用します。`File` オブジェクトまたはパスを受け付けます。 + +```typescript +await filesystem.copy('myFile.txt', 'myFolder/myFile.txt'); +``` + +`myFolder` ディレクトリが存在しない場合は作成されます。 + +## ファイル情報 + +ファイル情報を取得するには `get()` Method を使用します。`File` オブジェクトを返します。 + +```typescript +const file: FilesystemFile = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); +``` + +パス、バイト単位のファイルサイズ、最終更新日時を返します。アダプタがサポートしていない場合、更新日時は undefined の可能性があります。 + +File オブジェクトには便利な Method もいくつかあります: + +```typescript +interface FilesystemFile { + path: string; + type: FileType; //ファイルまたはディレクトリ + size: number; + lastModified?: Date; + /** + * ファイルの可視性。 + * + * 一部のアダプタはファイルの可視性の読み取りをサポートしていない場合があります。 + * この場合、可視性は常に 'private' になります。 + * + * 一部のアダプタはファイルごとの可視性の読み取りをサポートしますが、一覧時にはサポートしない場合があります。 + * この場合、可視性を読み込むために追加で `filesystem.get(file)` を呼び出す必要があります。 + */ + visibility: FileVisibility; //public または private + constructor(path: string, type?: FileType); + /** + * このファイルがシンボリックリンクの場合に true を返します。 + */ + isFile(): boolean; + /** + * このファイルがディレクトリの場合に true を返します。 + */ + isDirectory(): boolean; + /** + * ファイルの名前(ベース名)を返します。 + */ + get name(): string; + /** + * このファイルが指定されたディレクトリ内にある場合に true を返します。 + */ + inDirectory(directory: string): boolean; + /** + * ファイルのディレクトリ名(dirname)を返します。 + */ + get directory(): string; + /** + * ファイルの拡張子を返します。存在しない場合やディレクトリの場合は空文字列を返します。 + */ + get extension(): string; +} +``` + +## ファイルの存在確認 + +ファイルの存在を確認するには `exists()` Method を使用します。 + +```typescript + +if (await filesystem.exists('myFile.txt')) { + console.log('File exists'); +} +``` + +## ファイルの可視性 + +Deepkit Filesystems は、ファイルを public または private にできるシンプルなファイル可視性の抽象化をサポートしています。 + +これは、例えば S3 や Google Cloud Filesystem に便利です。ローカルファイルシステムでは、可視性に応じてファイルのパーミッションを設定します。 + +ファイルの可視性を設定するには、`setVisibility()` Method を使用するか、`write()` の第3引数として可視性を渡します。 + +```typescript +await filesystem.setVisibility('myFile.txt', 'public'); +await filesystem.write('myFile.txt', 'Hello World', 'public'); +``` + +ファイルの可視性を取得するには、`getVisibility()` Method を使用するか、`FilesystemFile.visibility` を確認します。 + +```typescript +const visibility = await filesystem.getVisibility('myFile.txt'); +const file = await filesystem.get('myFile.txt'); +console.log(file.visibility); +``` + +一部のアダプタは `sotrage.files()` から可視性の取得をサポートしていない場合があります。この場合、可視性は常に `unknown` です。 + +## ファイルの公開 URL + +ファイルの公開 URL を取得するには `publicUrl()` Method を使用します。これはファイルが public であり、かつアダプタがそれをサポートしている場合にのみ機能します。 + +```typescript +const url = filesystem.publicUrl('myFile.txt'); +``` + +アダプタが公開 URL をサポートしていない場合、Filesystem の抽象化は `option.baseUrl` を使用して URL を生成して対応します。 + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + baseUrl: 'https://my-domain.com/assets/' +}); + +const url = await filesystem.publicUrl('myFile.txt'); +console.log(url); //https://my-domain.com/assets/myFile.txt +``` + +## Filesystem のオプション + +`Filesystem` のコンストラクタは第2引数にオプションを受け取ります。 + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + visibility: 'private', //ファイルのデフォルト可視性 + directoryVisibility: 'private', //ディレクトリのデフォルト可視性 + pathNormalizer: (path: string) => path, //パスを正規化します。デフォルトでは `[^a-zA-Z0-9\.\-\_]` を `-` に置き換えます。 + urlBuilder: (path: string) => path, //ファイルの公開 URL を構築します。デフォルトでは baseUrl + path を返します + baseUrl: '', //公開 URL 用のベース URL +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/app.md b/website/src/translations/ja/documentation/filesystem/app.md new file mode 100644 index 000000000..22ca8e8c7 --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/app.md @@ -0,0 +1,83 @@ +# アプリ + +Deepkit Storage はスタンドアロンでも動作しますが、通常は依存性注入を用いて Deepkit アプリ内で使用したくなるでしょう。 + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +const app = new App({ + providers: [ + provideFilesystem(new FilesystemLocalAdapter({root: __dirname + '/public'})), + ] +}); + +app.command('write', async (content: string, fs: Filesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: Filesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + + +## 複数のファイルシステム + +同時に複数のファイルシステムを使用できます。このためには、`provideNamedFilesystem('name', ...)` で登録し、`NamedFilesystem<'name'>` で Filesystem インスタンスを受け取ります。 + +```typescript +import { App } from '@deepkit/app'; +import { NamedFilesystem, FilesystemLocalAdapter, provideNamedFilesystem } from '@deepkit/filesystem'; + +type PrivateFilesystem = NamedFilesystem<'private'>; +type PublicFilesystem = NamedFilesystem<'public'>; + +const app = new App({ + providers: [ + provideNamedFilesystem('private', new FilesystemLocalAdapter({root: '/tmp/dir1'})), + provideNamedFilesystem('public', new FilesystemLocalAdapter({root: '/tmp/dir2'})), + ] +}); + +app.command('write', async (content: string, fs: PublicFilesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: PublicFilesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + +## 設定 + +設定オプションを通してアダプタを設定できると便利なことがよくあります。例えば、ローカルアダプタのルートディレクトリや、AWS S3 や Google Storage アダプタ用の認証情報を設定したい場合があります。 + +そのためには、[設定のインジェクション](../app/configuration.md) を使用できます。 + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +class MyConfig { + fsRoot: string = '/tmp'; +} + +const app = new App({ + config: MyConfig, + providers: [ + provideFilesystem( + (config: MyConfig) => new FilesystemLocalAdapter({root: config.fsRoot}) + ), + ] +}); + +app.loadConfigFromEnv({prefix: 'APP_'}) +app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/aws-s3.md b/website/src/translations/ja/documentation/filesystem/aws-s3.md new file mode 100644 index 000000000..36f85412b --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/aws-s3.md @@ -0,0 +1,50 @@ +# AWS S3 ファイルシステム + +AWS S3 ファイルシステムアダプターを使用すると、AWS S3 サービスを Deepkit Filesystem として使用できます。 + +これは `@deepkit/filesystem-aws-s3` の一部で、別途インストールが必要です。 + +```sh +npm install @deepkit/filesystem-aws-s3 +``` + +## 使用方法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemAwsS3Adapter } from '@deepkit/filesystem-aws-s3'; + +const adapter = new FilesystemAwsS3Adapter({ + bucket: 'my-bucket', + path: 'starting-path/', // 省略可 + region: 'eu-central-1', + acccessKeyId: '...', + secretAccessKey: '...' +}); +const filesystem = new Filesystem(adapter); +``` + +注意: 資格情報をコード内に直接保存しないでください。代わりに、環境変数または[アプリ構成](./app.md#configuration)を使用してください。 + +このアダプターは [@aws-sdk/client-s3](https://npmjs.com/package/@aws-sdk/client-s3) の S3 クライアントを使用します。 +そのすべての設定オプションをアダプターのコンストラクターに渡せます。 + +## 権限 + +ファイル作成時の可視性を構成できます。 + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +ファイル `/hello-public.txt` は ACL `public-read` で作成され、その URL を使用して誰でも読み取り可能です。URL は `filesystem.publicUrl` を使用して取得できます。 + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://my-bucket.s3.eu-central-1.amazonaws.com/starting-path/hello-public.txt +``` + +この可視性を使用するには、S3 バケットでオブジェクトベースの ACL を有効にする必要があります。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/database.md b/website/src/translations/ja/documentation/filesystem/database.md new file mode 100644 index 000000000..5deeddf87 --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/database.md @@ -0,0 +1,23 @@ +# データベースファイルシステム + +このアダプターにより、データベース ORM をファイルシステムのバックエンドとして使用できます。これは、すべてのファイルとフォルダーがデータベースに保存されることを意味します。 + +```sh +npm install @deepkit/filesystem-database @deepkit/orm +``` + +## 使用方法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemDatabaseAdapter } from '@deepkit/filesystem-database'; + +const database = new Database(new MemoryDatabaseAdapter()); +// const database = new Database(new PostgresDatabaseAdapter()); +// const database = new Database(new MongoDatabaseAdapter()); +// const database = new Database(new MysqlDatabaseAdapter()); +// const database = new Database(new SQLiteDatabaseAdapter()); + +const adapter = new FilesystemDatabaseAdapter({ database }); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/ftp.md b/website/src/translations/ja/documentation/filesystem/ftp.md new file mode 100644 index 000000000..832122518 --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/ftp.md @@ -0,0 +1,55 @@ +# FTP ファイルシステム + +このアダプタを使用すると、FTP サーバーをファイルシステムとして利用できます。 + +これは `@deepkit/filesystem-ftp` の一部であり、別途インストールする必要があります。 + +```sh +npm install @deepkit/filesystem-ftp +``` + +## 使用方法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemFtpAdapter } from '@deepkit/filesystem-ftp'; + +const adapter = new FilesystemFtpAdapter({ + root: 'folder', + host: 'localhost', + port: 21, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +注意: 認証情報をコードに直接保存すべきではありません。代わりに、環境変数や[アプリ設定](./app.md#configuration)を使用してください。 + +## パーミッション + +FTP サーバーが Unix 環境で動作している場合、[ローカルファイルシステムアダプタ](./local.md) と同様に、`permissions` オプションを使用してファイルやフォルダのパーミッションを設定できます。 + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +ここでは、ファイル `/hello-public.txt` はパーミッション `0o644` で、`/hello-private.txt` は `0o600` で作成されます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/google-storage.md b/website/src/translations/ja/documentation/filesystem/google-storage.md new file mode 100644 index 000000000..76f57686e --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/google-storage.md @@ -0,0 +1,49 @@ +# Google Storage ファイルシステム + +このアダプターは、Google Storage をファイルシステムとして使用できるようにします。 + +これは `@deepkit/filesystem-google` の一部で、別途インストールが必要です。 + +```sh +npm install @deepkit/filesystem-google +``` + +## 使用方法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemGoogleAdapter } from '@deepkit/filesystem-google'; + +const adapter = new FilesystemGoogleAdapter({ + bucket: 'my-bucket', + path: 'starting-path/', //optional + projectId: '...', + keyFilename: 'path/to/service-account-key.json' +}); +const filesystem = new Filesystem(adapter); +``` + +注意: 認証情報をコード内に直接保存すべきではありません。代わりに、環境変数または[アプリケーション設定](./app.md#configuration)を使用してください。 + +このアダプターは [@google-cloud/storage](https://npmjs.com/package/@google-cloud/storage) の Google Storage クライアントを使用します。 +そのすべての設定オプションをアダプターのコンストラクターに渡せます。 + +## 権限 + +作成時のファイルの可視性を設定できます。 + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +ファイル `/hello-public.txt` は ACL `public: true` で作成され、URL を使用して誰でも読み取ることができます。URL は `filesystem.publicUrl` を使用して取得できます。 + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://storage.googleapis.com/my-bucket/starting-path/hello-public.txt +``` + +可視性を利用するには、Google Storage バケットでオブジェクトベースの ACL を有効にする必要があります。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/local.md b/website/src/translations/ja/documentation/filesystem/local.md new file mode 100644 index 000000000..f63346d65 --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/local.md @@ -0,0 +1,49 @@ +# ローカルファイルシステム + +ローカルファイルシステムアダプターは最も一般的なファイルシステムの一つで、アプリケーションが実行されているシステム上のファイルシステムへのアクセスを提供します。 + +これは `@deepkit/filesystem` の一部で、内部的に Node の `fs/promises` API を使用するため、追加のインストールは不要です。 + +## 使い方 + +```typescript +import { FilesystemLocalAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter({ root: '/path/to/files' }); +const filesystem = new Filesystem(adapter); +``` + +ここで `root` オプションはファイルシステムのルートディレクトリを指します。すべてのパスはこのルートからの相対パスです。 + +```typescript +// /path/to/files/hello.txt のファイルを読み込みます +const content: string = await filesystem.readAsText('/hello.txt'); +``` + +## パーミッション + +ファイルやディレクトリを作成する際に、ファイルシステムが使用するパーミッションを設定できます。各カテゴリ(file, directory)は、`public` と `private` の2つの可視性に対して個別に設定できます。 + +```typescript +const adapter = new FilesystemLocalAdapter({ + root: '/path/to/files', + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +ここでは、`/hello-public.txt` はパーミッション `0o644` で、`/hello-private.txt` は `0o600` で作成されます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/memory.md b/website/src/translations/ja/documentation/filesystem/memory.md new file mode 100644 index 000000000..009cf973b --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/memory.md @@ -0,0 +1,15 @@ +# メモリファイルシステム + +メモリファイルシステムでは、ファイルシステムはメモリ上に保存されます。これは、ファイルシステムが永続化されず、アプリケーションを終了すると失われることを意味します。 +特にテスト用途に有用です。 + +これは `@deepkit/filesystem` の一部であり、追加のインストールは不要です。 + +## 使い方 + +```typescript +import { FilesystemMemoryAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemMemoryAdapter(); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/filesystem/sftp.md b/website/src/translations/ja/documentation/filesystem/sftp.md new file mode 100644 index 000000000..a630d265a --- /dev/null +++ b/website/src/translations/ja/documentation/filesystem/sftp.md @@ -0,0 +1,57 @@ +# sFTP (SSH) ファイルシステム + +このアダプタにより、sFTP (SSH) サーバーをファイルシステムとして使用できます。 + +これは `@deepkit/filesystem-sftp` の一部であり、別途インストールする必要があります。 + +```sh +npm install @deepkit/filesystem-sftp +``` + +## 使用方法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemSftpAdapter } from '@deepkit/filesystem-sftp'; + +const adapter = new FilesystemSftpAdapter({ + root: 'folder', + host: 'localhost', + port: 22, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +注意: 認証情報をコード内に直接保存するべきではありません。代わりに、環境変数または[アプリケーション設定](./app.md#configuration)を使用してください。 + +このアダプタは [ssh2-sftp-client](https://npmjs.com/package/ssh2-sftp-client) の sFTP クライアントを使用します。そのすべての設定オプションをアダプタのコンストラクタに渡すことができます。 + +## 権限 + +FTP サーバーが Unix 環境で実行されている場合、[ローカルファイルシステムアダプタ](./local.md) と同様に、`permissions` オプションを使用してファイルとフォルダーの権限を設定できます。 + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +ここでは、ファイル `/hello-public.txt` は権限 `0o644` で、`/hello-private.txt` は `0o600` で作成されます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/framework.md b/website/src/translations/ja/documentation/framework.md new file mode 100644 index 000000000..657d3c7c6 --- /dev/null +++ b/website/src/translations/ja/documentation/framework.md @@ -0,0 +1,215 @@ +# Deepkit Framework + +Deepkit Framework は `@deepkit/app` の [Deepkit App](./app.md) を基盤としており、`@deepkit/framework` の `FrameworkModule` モジュールを提供します。これはアプリケーションにインポートできます。 + +`App` 抽象は以下を提供します: + +- CLI コマンド +- 設定の読み込み(環境変数、ドットファイル、カスタム) +- モジュールシステム +- 強力なサービスコンテナ +- コントローラー、プロバイダー、リスナーなどのためのレジストリとフック + +`FrameworkModule` モジュールは追加の機能を提供します: + +- アプリケーションサーバー + - HTTP サーバー + - RPC サーバー + - マルチプロセス負荷分散 + - SSL +- デバッグ用 CLI コマンド +- データベースマイグレーションの設定/コマンド +- `{debug: true}` オプションによるデバッグ/プロファイラー GUI +- インタラクティブな API ドキュメント(Swagger のような) +- DatabaseRegistry、ProcessLocking、Broker、Sessions 用のプロバイダー +- 統合テスト用 API + +`FrameworkModule` の有無にかかわらずアプリケーションを作成できます。 + +## インストール + +Deepkit Framework は [Deepkit App](./app.md) を基盤としています。そのインストール手順に従っていることを確認してください。 +完了していれば、Deepkit Framework をインストールし、`App` に `FrameworkModule` をインポートできます。 + +```sh +npm install @deepkit/framework +``` + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +アプリが `FrameworkModule` をインポートしたため、トピックごとにグループ化された追加のコマンドが利用可能になっているのが分かります。 + +そのひとつが `server:start` で、HTTP サーバーを起動します。これを使用するには、少なくとも 1 つの HTTP ルートを登録する必要があります。 + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return 'Hello World'; +}) + +app.run(); +``` + +再度 `server:start` コマンドを実行すると、HTTP サーバーが起動し、ルート `/` が利用可能になっていることが分かります。 + +```sh +$ ./node_modules/.bin/ts-node ./app.ts server:start +``` + +```sh +$ curl http://localhost:8080/ +Hello World +``` + +リクエストの処理方法については、[HTTP](http.md) 章または [RPC](rpc.md) 章を参照してください。[App](app.md) 章では CLI コマンドについてさらに学べます。 + +## App + +`App` クラスはアプリケーションのメインエントリポイントです。すべてのモジュールや設定の読み込み、アプリケーションの起動を担当します。 +また、すべての CLI コマンドの読み込みと実行も担当します。`FrameworkModule` のようなモジュールは、追加のコマンドを提供し、イベントリスナーを登録し、 +HTTP/RPC 用のコントローラー、サービスプロバイダーなどを提供します。 + +この `app` オブジェクトは、CLI コントローラーを実行せずに依存性注入コンテナへアクセスするためにも使用できます。 + +```typescript +const app = new App({ + imports: [new FrameworkModule] +}); + +//get access to all registered services +const eventDispatcher = app.get(EventDispatcher); +``` + +`FrameworkModule` が多くのもの(Logger、ApplicationServer、そして[その他多数](https://github.com/deepkit/deepkit-framework/blob/master/packages/framework/src/module.ts))と同様に `EventDispatcher` をサービスプロバイダーとして登録しているため、`EventDispatcher` を取得できます。 + +独自のサービスを登録することもできます。 + +```typescript + +class MyService { + constructor(private logger: Logger) { + } + + helloWorld() { + this.logger.log('Hello World'); + } +} + +const app = new App({ + providers: [MyService], + imports: [new FrameworkModule] +}); + +const service = app.get(MyService); + +service.helloWorld(); +``` + +### デバッガー + +アプリケーションおよびすべてのモジュールの設定値はデバッガーで表示できます。`FrameworkModule` の debug オプションを有効にし、`http://localhost:8080/_debug/configuration` を開きます。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + debug: true, + }) + ] +}).run(); +``` + +![デバッガーの設定](/assets/documentation/framework/debugger-configuration.png) + +また、`ts-node app.ts app:config` を使用して、利用可能なすべての設定オプション、現在の値、デフォルト値、説明、データ型を表示することもできます。 + +```sh +$ ts-node app.ts app:config +Application config +┌─────────┬───────────────┬────────────────────────┬────────────────────────┬─────────────┬───────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼───────────────┼────────────────────────┼────────────────────────┼─────────────┼───────────┤ +│ 0 │ 'pageTitle' │ 'Other title' │ 'Cool site' │ '' │ 'string' │ +│ 1 │ 'domain' │ 'example.com' │ 'example.com' │ '' │ 'string' │ +│ 2 │ 'port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 3 │ 'databaseUrl' │ 'mongodb://localhost/' │ 'mongodb://localhost/' │ '' │ 'string' │ +│ 4 │ 'email' │ false │ false │ '' │ 'boolean' │ +│ 5 │ 'emailSender' │ undefined │ undefined │ '' │ 'string?' │ +└─────────┴───────────────┴────────────────────────┴────────────────────────┴─────────────┴───────────┘ +Modules config +┌─────────┬──────────────────────────────┬─────────────────┬─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼──────────────────────────────┼─────────────────┼─────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────┤ +│ 0 │ 'framework.host' │ 'localhost' │ 'localhost' │ '' │ 'string' │ +│ 1 │ 'framework.port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 2 │ 'framework.httpsPort' │ undefined │ undefined │ 'If httpsPort and ssl is defined, then the https server is started additional to the http-server.' │ 'number?' │ +│ 3 │ 'framework.selfSigned' │ undefined │ undefined │ 'If for ssl: true the certificate and key should be automatically generated.' │ 'boolean?' │ +│ 4 │ 'framework.keepAliveTimeout' │ undefined │ undefined │ '' │ 'number?' │ +│ 5 │ 'framework.path' │ '/' │ '/' │ '' │ 'string' │ +│ 6 │ 'framework.workers' │ 1 │ 1 │ '' │ 'number' │ +│ 7 │ 'framework.ssl' │ false │ false │ 'Enables HTTPS server' │ 'boolean' │ +│ 8 │ 'framework.sslOptions' │ undefined │ undefined │ 'Same interface as tls.SecureContextOptions & tls.TlsOptions.' │ 'any' │ +... +``` + +## アプリケーションサーバー + +## ファイル構造 + +## 自動 CRUD + +## イベント + +Deepkit フレームワークには、イベントリスナーを登録できるさまざまなイベントトークンが用意されています。 + +イベントの仕組みについて詳しくは、[イベント](./app/events.md) 章を参照してください。 + +### イベントのディスパッチ + +イベントは `EventDispatcher` クラスを介して送信されます。Deepkit アプリでは、これは依存性注入を通じて提供されます。 + +```typescript +import { cli, Command } from '@deepkit/app'; +import { EventDispatcher } from '@deepkit/event'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected eventDispatcher: EventDispatcher) { + } + + async execute() { + this.eventDispatcher.dispatch(UserAdded, new UserEvent({ username: 'Peter' })); + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/framework/database.md b/website/src/translations/ja/documentation/framework/database.md new file mode 100644 index 000000000..7e9773733 --- /dev/null +++ b/website/src/translations/ja/documentation/framework/database.md @@ -0,0 +1,225 @@ +# データベース + +Deepkit には、Deepkit ORM と呼ばれる強力なデータベース抽象化ライブラリがあります。これは、SQL データベースや MongoDB の操作を容易にする Object-Relational Mapping(ORM)ライブラリです。 + +任意のデータベースライブラリを使用できますが、Deepkit フレームワークと完全に統合され、ワークフローと効率を向上させる多数の機能を備え、TypeScript で最速のデータベース抽象化ライブラリであるため、Deepkit ORM を推奨します。 + +この章では、Deepkit アプリで Deepkit ORM を使用する方法を説明します。Deepkit ORM の詳細については、[ORM](../orm.md) の章を参照してください。 + +## データベースクラス + +アプリケーション内で Deepkit ORM の `Database` オブジェクトを使用する最も簡単な方法は、それを継承したクラスを登録することです。 + +```typescript +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + constructor() { + super( + new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), + [User] + ); + } +} +``` + +新しいクラスを作成し、そのコンストラクタでパラメータ付きのアダプタを指定し、このデータベースに接続すべきすべてのエンティティモデルを第 2 引数に追加します。 + +このデータベースクラスをプロバイダーとして登録できます。さらに、`migrateOnStartup` を有効にすると、ブートストラップ時にデータベース内のすべてのテーブルが自動的に作成されます。これは迅速なプロトタイピングに最適ですが、本格的なプロジェクトや本番環境では推奨されません。通常のデータベースマイグレーションを使用してください。 + +また `debug` も有効にします。これにより、アプリケーションのサーバー起動時にデバッガを開き、組み込みの ORM ブラウザでデータベースモデルを直接管理できます。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { SQLiteDatabase } from './database.ts'; + +new App({ + providers: [SQLiteDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}).run(); +``` + +Dependency Injection を使用して、どこからでも `SQLiteDatabase` にアクセスできます: + +```typescript +import { SQLiteDatabase } from './database.ts'; + +export class Controller { + constructor(protected database: SQLiteDatabase) {} + + @http.GET() + async startPage(): Promise<User[]> { + // すべてのユーザーを返す + return await this.database.query(User).find(); + } +} +``` + +## 設定 + +多くの場合、接続認証情報を設定可能にしたいでしょう。例えば、本番とは異なるデータベースをテストで使用したい場合です。これは `Database` クラスの `config` オプションを用いることで実現できます。 + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { PostgresDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +type DbConfig = Pick<AppConfig, 'databaseHost', 'databaseUser', 'databasePassword'>; + +export class MainDatabase extends Database { + constructor(config: DbConfig) { + super(new PostgresDatabaseAdapter({ + host: config.databaseHost, + user: config.databaseUser, + password: config.databasePassword, + }), [User]); + } +} +``` + +```typescript +//config.ts +export class AppConfig { + databaseHost: string = 'localhost'; + databaseUser: string = 'postgres'; + databasePassword: string = ''; +} +``` + +```typescript +//app.ts +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { MainDatabase } from './database.ts'; +import { AppConfig } from './config.ts'; + +const app = new App({ + config: AppConfig, + providers: [MainDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}); +app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper', envFilePath: ['local.env', 'prod.env']}); +app.run(); +``` + +これで、`loadConfigFromEnv` を使用しているため、環境変数を通じてデータベースの認証情報を設定できます。 + +```sh +APP_DATABASE_HOST=localhost APP_DATABASE_USER=postgres ts-node app.ts server:start +``` + +または `local.env` ファイルに記述し、事前に環境変数を設定せずに `ts-node app.ts server:start` を起動します。 + +```sh +APP_DATABASE_HOST=localhost +APP_DATABASE_USER=postgres +``` + +## 複数のデータベース + +必要なだけデータベースクラスを追加し、任意の名前を付けられます。Deepkit ORM Browser を使用する際に他と衝突しないよう、各データベースの名前を必ず変更してください。 + +## データの管理 + +これで Deepkit ORM Browser を使ってデータベースのデータを管理する準備が整いました。Deepkit ORM Browser を開いて内容を管理するには、上記のすべての手順を `app.ts` に記述し、サーバーを起動します。 + +```sh +$ ts-node app.ts server:start +2021-06-11T15:08:54.330Z [LOG] Start HTTP server, using 1 workers. +2021-06-11T15:08:54.333Z [LOG] Migrate database default +2021-06-11T15:08:54.336Z [LOG] RPC DebugController deepkit/debug/controller +2021-06-11T15:08:54.337Z [LOG] RPC OrmBrowserController orm-browser/controller +2021-06-11T15:08:54.337Z [LOG] HTTP OrmBrowserController +2021-06-11T15:08:54.337Z [LOG] GET /_orm-browser/query httpQuery +2021-06-11T15:08:54.337Z [LOG] HTTP StaticController +2021-06-11T15:08:54.337Z [LOG] GET /_debug/:any serviceApp +2021-06-11T15:08:54.337Z [LOG] HTTP listening at http://localhost:8080/ +``` + +http://localhost:8080/_debug/database/default を開けます。 + +![デバッガ データベース](/assets/documentation/framework/debugger-database.png) + +ER(エンティティリレーションシップ)図が表示されます。現在は 1 つのエンティティしかありません。リレーションを持つものを追加すると、すべての情報が一目で分かります。 + +左側のサイドバーで `User` をクリックすると、その内容を管理できます。`+` アイコンをクリックして新しいレコードのタイトルを変更します。必要な値(ユーザー名など)を変更したら、`Confirm` をクリックします。これにより、すべての変更がデータベースにコミットされ、永続化されます。自動採番された ID は自動的に割り当てられます。 + +![デバッガ データベース ユーザー](/assets/documentation/framework/debugger-database-user.png) + +## さらに詳しく + +`SQLiteDatabase` の動作についてさらに知るには、[データベース](../orm.md) の章と、そのサブチャプター(データのクエリ、セッションを介したデータ操作、リレーションの定義など)を参照してください。 +なお、そこにある章はスタンドアロンライブラリ `@deepkit/orm` を対象としており、この章で説明した Deepkit フレームワークの部分のドキュメントは含まれていません。スタンドアロンライブラリでは、例えば `new SQLiteDatabase()` のようにデータベースクラスを手動でインスタンス化します。しかし Deepkit アプリでは、これは Dependency Injection コンテナによって自動的に行われます。 + +## マイグレーション + +Deepkit フレームワークには、マイグレーションの作成・実行・取り消しを可能にする強力なマイグレーションシステムがあります。マイグレーションシステムは Deepkit ORM ライブラリに基づいており、フレームワークに完全に統合されています。 + +`FrameworkModule` はマイグレーションを管理するための複数のコマンドを提供します。 + +- `migration:create` - データベースの差分に基づいて新しいマイグレーションファイルを生成します +- `migration:pending` - 保留中のマイグレーションファイルを表示します +- `migration:up` - 保留中のマイグレーションファイルを実行します。 +- `migration:down` - ダウンマイグレーションを実行し、古いマイグレーションを取り消します + +```sh +ts-node app.ts migration:create --migrationDir src/migrations +``` + +新しいマイグレーションファイルが `migrations` に作成されます。このフォルダは FrameworkModule で設定されているデフォルトのディレクトリです。変更するには、環境変数([設定](configuration.md) の章で説明)で構成を変更するか、`FrameworkModule` のコンストラクタに `migrationDir` オプションを渡します。 + +```typescript +new FrameworkModule({ + migrationDir: 'src/migrations', +}) +``` + +新しく作成されたマイグレーションファイルには、TypeScript アプリで定義されたエンティティと設定済みデータベースの差分に基づく up メソッドと down メソッドが含まれています。 +必要に応じて up メソッドを変更できます。down メソッドは up メソッドに基づいて自動生成されます。 +このファイルをリポジトリにコミットし、他の開発者も実行できるようにします。 + +### 保留中のマイグレーション + +```sh +ts-node app.ts migration:pending --migrationDir src/migrations +``` + +保留中のマイグレーションがすべて表示されます。まだ実行されていない新しいマイグレーションファイルがある場合、ここに表示されます。 + +### マイグレーションの実行 + +```sh +ts-node app.ts migration:up --migrationDir src/migrations +``` + +次の保留中のマイグレーションを実行します。 + +### マイグレーションの取り消し + +```sh +ts-node app.ts migration:down --migrationDir src/migrations +``` + +最後に実行したマイグレーションを取り消します。 + +### 疑似マイグレーション + +たとえば、あるマイグレーション(up または down)を実行しようとして失敗したとします。問題を手作業で修正したものの、そのマイグレーションはすでに実行済みとして扱われるため、再実行できません。`--fake` オプションを使うと、実際には実行せずにデータベース上で実行済みとして印を付けることができます。これにより、次の保留中のマイグレーションを実行できるようになります。 + +```sh +ts-node app.ts migration:up --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/framework/deployment.md b/website/src/translations/ja/documentation/framework/deployment.md new file mode 100644 index 000000000..124bc04b3 --- /dev/null +++ b/website/src/translations/ja/documentation/framework/deployment.md @@ -0,0 +1,138 @@ +# デプロイ + +この章では、アプリケーションを JavaScript にコンパイルし、本番環境向けに設定し、Docker を使用してデプロイする方法を学びます。 + +## TypeScript をコンパイルする + +`app.ts` ファイルに次のようなアプリケーションがあるとします: + +```typescript +#!/usr/bin/env ts-node-script +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class Config { + title: string = 'DEV my Page'; +} + +class MyWebsite { + constructor(protected title: Config['title']) { + } + + @http.GET() + helloWorld() { + return 'Hello from ' + this.title; + } +} + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + .loadConfigFromEnv() + .run(); +``` + +`ts-node app.ts server:start` を使用すると、すべてが正しく動作することがわかります。本番環境では通常、`ts-node` でサーバーを起動しません。JavaScript にコンパイルしてから Node で実行します。そのためには、適切な設定オプションを備えた正しい `tsconfig.json` が必要です。「First Application」セクションでは、`tsconfig.json` は JavaScript を `.dist` フォルダーに出力するように設定されています。あなたも同様に設定しているものとします。 + +コンパイラ設定がすべて正しく、`outDir` が `dist` などのフォルダーを指していれば、プロジェクトで `tsc` コマンドを実行した瞬間に、`tsconfig.json` にリンクされたファイルはすべて JavaScript にコンパイルされます。このリストにはエントリーファイルを指定するだけで十分です。インポートされるファイルも自動的にコンパイルされるため、`tsconfig.json` に明示的に追加する必要はありません。`tsc` は `npm install typescript` をインストールすると含まれる TypeScript の一部です。 + +```sh +$ ./node_modules/.bin/tsc +``` + +TypeScript コンパイラは成功した場合、何も出力しません。`dist` の出力を確認できます。 + +```sh +$ tree dist +dist +└── app.js +``` + +ファイルが 1 つだけであることがわかります。`node distapp.js` で実行すれば、`ts-node app.ts` と同じ機能が得られます。 + +デプロイでは、TypeScript ファイルが正しくコンパイルされ、Node から直接すべてが動作することが重要です。`node_modules` を含む `dist` フォルダーをそのまま移動し、`node distapp.js server:start` を実行すれば、アプリは正常にデプロイされます。しかし、アプリを正しくパッケージ化するために Docker のような他のソリューションを使用するのが一般的です。 + +## 設定 + +本番環境では、サーバーを `localhost` にバインドするのではなく、通常は `0.0.0.0` で全デバイスにバインドします。リバースプロキシの背後にいない場合は、ポートを 80 に設定するでしょう。これら 2 つの設定を構成するには、`FrameworkModule` をカスタマイズする必要があります。対象となる 2 つのオプションは `host` と `port` です。それらを環境変数や .dotenv ファイル経由で外部から設定できるようにするには、まずそれを許可する必要があります。幸い、上記のコードは `loadConfigFromEnv()` メソッドで既にそれを行っています。 + +アプリケーションの設定オプションの設定方法について詳しくは、[設定](../app/configuration.md) の章を参照してください。 + +利用可能な設定オプションとその値を確認するには、`ts-node app.ts app:config` コマンドを使用できます。Framework Debugger でも確認できます。 + +### SSL + +アプリケーションを SSL を用いた HTTPS で実行することが推奨され(場合によっては必須)です。SSL を構成するためのオプションがいくつかあります。SSL を有効にするには +`framework.ssl` を使用し、次のオプションでそのパラメータを設定します。 + +|=== +|名前|Type|説明 + +|framework.ssl|boolean|true の場合、HTTPS サーバーを有効にします +|framework.httpsPort|number?|httpsPort と ssl が定義されている場合、http サーバーに加えて https サーバーも起動されます。 +|framework.sslKey|string?|https 用の SSL キーファイルへのファイルパス +|framework.sslCertificate|string?|https 用の証明書ファイルへのファイルパス +|framework.sslCa|string?|https 用の CA ファイルへのファイルパス +|framework.sslCrl|string?|https 用の CRL ファイルへのファイルパス +|framework.sslOptions|object?|tls.SecureContextOptions および tls.TlsOptions と同じ Interface。 +|=== + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// ここに config と HTTP コントローラーを記述 + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + sslKey: __dirname + 'path/ssl.key', + sslCertificate: __dirname + 'path/ssl.cert', + sslCA: __dirname + 'path/ssl.ca', + }) + ] +}) + .run(); +``` + +### ローカル SSL + +ローカル開発環境では、`framework.selfSigned` オプションで自己署名の HTTPS を有効にできます。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// ここに config と HTTP コントローラーを記述 + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + }) + ] +}) + .run(); +``` + +```sh +$ ts-node app.ts server:start +2021-06-13T18:04:01.563Z [LOG] Start HTTP server, using 1 workers. +2021-06-13T18:04:01.598Z [LOG] Self signed certificate for localhost created at var/self-signed-localhost.cert +2021-06-13T18:04:01.598Z [LOG] Tip: If you want to open this server via chrome for localhost, use chrome://flags/#allow-insecure-localhost +2021-06-13T18:04:01.606Z [LOG] HTTP MyWebsite +2021-06-13T18:04:01.606Z [LOG] GET / helloWorld +2021-06-13T18:04:01.606Z [LOG] HTTPS listening at https://localhost:8080/ +``` + +このサーバーを今起動すると、HTTP サーバーは `https:localhost:8080` で HTTPS として利用可能になります。自己署名証明書はセキュリティリスクとみなされるため、この URL を Chrome で開くとエラーメッセージ「NET::ERR_CERT_INVALID」が表示されます: `chrome:flagsallow-insecure-localhost`。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/framework/public.md b/website/src/translations/ja/documentation/framework/public.md new file mode 100644 index 000000000..0c16f4446 --- /dev/null +++ b/website/src/translations/ja/documentation/framework/public.md @@ -0,0 +1,34 @@ +# 公開ディレクトリ + +`FrameworkModule` は、画像、PDF、バイナリなどの静的ファイルを HTTP 経由で配信する方法を提供します。`publicDir` 設定オプションにより、HTTP コントローラーのルートに至らないリクエストに対するデフォルトのエントリーポイントとして使用するフォルダーを指定できます。デフォルトでは、この動作は無効(空の値)になっています。 + +公開ファイルの提供を有効にするには、`publicDir` を任意のフォルダーに設定します。通常は、分かりやすいように `publicDir` のような名前を選びます。 + +``` +. +├── app.ts +└── publicDir + └── logo.jpg +``` + +`publicDir` オプションを変更するには、`FrameworkModule` の最初の引数を変更します。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// your config and http controller here + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + publicDir: 'publicDir' + }) + ] +}) + .run(); +``` + +これで、この設定済みフォルダー内のすべてのファイルに HTTP 経由でアクセスできるようになります。たとえば、`http:localhost:8080/logo.jpg` を開くと、`publicDir` ディレクトリ内の `logo.jpg` という画像が表示されます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/framework/testing.md b/website/src/translations/ja/documentation/framework/testing.md new file mode 100644 index 000000000..d7e4a6dbf --- /dev/null +++ b/website/src/translations/ja/documentation/framework/testing.md @@ -0,0 +1,179 @@ +# テスト + +Deepkit フレームワークのサービスとコントローラーは、設計の行き届いたカプセル化と分離を備えた、SOLID かつクリーンなコードをサポートするように設計されています。これらの特性により、コードはテストしやすくなります。 + +このドキュメントでは、`ts-jest` を用いてテストフレームワーク [Jest](https://jestjs.io) をセットアップする方法を説明します。これを行うには、次のコマンドを実行して `jest` と `ts-jest` をインストールします。 + +```sh +npm install jest ts-jest @types/jest +``` + +Jest がテストスイートの場所や TS コードのコンパイル方法を認識できるように、いくつかの設定オプションが必要です。`package.json` に次の設定を追加してください: + +```json title=package.json +{ + ..., + + "jest": { + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "testEnvironment": "node", + "testMatch": [ + "**/*.spec.ts" + ] + } +} +``` + +テストファイルは `.spec.ts` という命名にしてください。次の内容で `test.spec.ts` ファイルを作成します。 + +```typescript +test('first test', () => { + expect(1 + 1).toBe(2); +}); +``` + +これで jest コマンドを使用して、すべてのテストスイートを一度に実行できます。 + +```sh +$ node_modules/.bin/jest + PASS ./test.spec.ts + ✓ first test (1 ms) + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: 0.23 s, estimated 1 s +Ran all test suites. +``` + +Jest CLI ツールの仕組みや、より高度なテストやテストスイート全体の書き方については [Jest-Dokumentation](https://jestjs.io) を参照してください。 + +## ユニットテスト + +可能な限り、サービスにはユニットテストを行うべきです。サービスの依存関係がシンプルで、適切に分離され、明確に定義されているほど、テストは容易になります。この場合、次のようなシンプルなテストが書けます。 + +```typescript +export class MyService { + helloWorld() { + return 'hello world'; + } +} +``` + +```typescript +// +import { MyService } from './my-service.ts'; + +test('hello world', () => { + const myService = new MyService(); + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +## 統合テスト + +ユニットテストが常に書けるとは限らず、ビジネス上重要なコードや挙動を網羅する最も効率的な方法であるとも限りません。特にアーキテクチャが非常に複雑な場合は、エンドツーエンドの統合テストを容易に実行できることが有益です。 + +依存性注入の章で学んだとおり、依存性注入コンテナは Deepkit の中核です。ここで全てのサービスが構築・実行されます。アプリケーションでは、サービス(プロバイダー)、コントローラー、リスナー、インポートを定義します。統合テストでは、テストケースで全てのサービスを利用可能にする必要は必ずしもなく、通常は重要な箇所をテストできる簡素化したアプリケーションのバージョンを用意したくなります。 + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { http, HttpRequest } from '@deepkit/http'; + +test('http controller', async () => { + class MyController { + + @http.GET() + hello(@http.query() text: string) { + return 'hello ' + text; + } + } + + const testing = createTestingApp({ controllers: [MyController] }); + await testing.startServer(); + + const response = await testing.request(HttpRequest.GET('/').query({text: 'world'})); + + expect(response.getHeader('content-type')).toBe('text/plain; charset=utf-8'); + expect(response.body.toString()).toBe('hello world'); +}); +``` + +```typescript +import { createTestingApp } from '@deepkit/framework'; + +test('service', async () => { + class MyService { + helloWorld() { + return 'hello world'; + } + } + + const testing = createTestingApp({ providers: [MyService] }); + + // 依存性注入コンテナにアクセスして MyService をインスタンス化する + const myService = testing.app.get(MyService); + + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +アプリケーションを複数のモジュールに分割している場合は、より容易にテストできます。たとえば、`AppCoreModule` を作成しており、いくつかのサービスをテストしたいとします。 + +```typescript +class Config { + items: number = 10; +} + +export class MyService { + constructor(protected items: Config['items']) { + + } + + doIt(): boolean { + // 何かを行う + return true; + } +} + +export AppCoreModule = new AppModule({}, { + config: config, + provides: [MyService] +}, 'core'); +``` + +モジュールは次のように使用します: + +```typescript +import { AppCoreModule } from './app-core.ts'; + +new App({ + imports: [new AppCoreModule] +}).run(); +``` + +そして、アプリケーションサーバー全体を起動せずにテストします。 + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { AppCoreModule, MyService } from './app-core.ts'; + +test('service simple', async () => { + const testing = createTestingApp({ imports: [new AppCoreModule] }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); + +test('service simple big', async () => { + // 特定のテストシナリオ向けにモジュールの設定を変更できます + const testing = createTestingApp({ + imports: [new AppCoreModule({items: 100})] + }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http.md b/website/src/translations/ja/documentation/http.md new file mode 100644 index 000000000..945b1c2b8 --- /dev/null +++ b/website/src/translations/ja/documentation/http.md @@ -0,0 +1,69 @@ +# HTTP + +HTTP リクエストの処理は、サーバーにとって最もよく知られたタスクの1つです。入力(HTTP リクエスト)を出力(HTTP レスポンス)に変換し、特定のタスクを実行します。クライアントは HTTP リクエストを介してさまざまな方法でサーバーにデータを送信でき、それらは正しく読み取り、適切に処理されなければなりません。HTTP ボディに加えて、HTTP クエリや HTTP ヘッダーの値も利用できます。データが実際にどのように処理されるかはサーバーに依存します。どこへ、どのように値をクライアントが送るかを定義するのはサーバーです。 + +ここで最優先されるのは、ユーザーが期待する処理を正しく実行することだけでなく、HTTP リクエストからのあらゆる入力を正しく変換(デシリアライズ)し、検証することです。 + +サーバー上で HTTP リクエストが通過するパイプラインは、多様で複雑になり得ます。多くのシンプルな HTTP ライブラリは、特定のルートに対して HTTP リクエストと HTTP レスポンスのみを渡し、開発者が HTTP レスポンスを直接処理することを想定しています。ミドルウェア API は、必要に応じてこのパイプラインを拡張できるようにします。 + +_Express の例_ + +```typescript +const http = express(); +http.get('/user/:id', (request, response) => { + response.send({id: request.params.id, username: 'Peter' ); +}); +``` + +これはシンプルなユースケースには非常に適していますが、アプリケーションが大きくなるにつれて、すべての入力と出力を手動でシリアライズ/デシリアライズし、検証しなければならないため、すぐに複雑になります。さらに、データベース抽象化のようなオブジェクトやサービスをアプリケーション自体からどのように取得するかも考慮する必要があります。これにより、これらの必須機能をマッピングするアーキテクチャを上に載せることを開発者に強いることになります。 + +これに対して、Deepkit の HTTP ライブラリは TypeScript と依存性注入の力を活用します。定義された型に基づいて、あらゆる値のシリアライズ/デシリアライズと検証が自動で行われます。また、上記の例のような関数型 API や、アーキテクチャのさまざまな要件に対応するためのコントローラークラスによってルートを定義することもできます。 + +これは、Node の `http` モジュールのような既存の HTTP サーバーでも、Deepkit フレームワークでも使用できます。どちらの API 形式も依存性注入コンテナにアクセスできるため、データベース抽象化や設定などのオブジェクトをアプリケーションから容易に取得できます。 + +## 関数型 API の例 + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; + +//関数型API +const app = new App({ + imports: [new FrameworkModule()] +}); +const router = app.get(HttpRouterRegistry); + +router.get('/user/:id', (id: number & Positive, database: Database) => { + //id は number かつ正の値であることが保証されています。 + //database は DI コンテナによって注入されます。 + return database.query(User).filter({ id }).findOne(); +}); + +app.run(); +``` + +## クラスコントローラー API + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; +import { User } from "discord.js"; + +//コントローラーAPI +class UserController { + constructor(private database: Database) { + } + + @http.GET('/user/:id') + user(id: number & Positive) { + return this.database.query(User).filter({ id }).findOne(); + } +} + +const app = new App({ + controllers: [UserController], + imports: [new FrameworkModule()] +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http/dependency-injection.md b/website/src/translations/ja/documentation/http/dependency-injection.md new file mode 100644 index 000000000..97cb92e20 --- /dev/null +++ b/website/src/translations/ja/documentation/http/dependency-injection.md @@ -0,0 +1,67 @@ +# 依存性注入 + +ルーターの関数、コントローラクラス、およびコントローラーメソッドは任意の依存関係を定義でき、これらは依存性注入コンテナによって解決されます。たとえば、データベース抽象化やロガーに簡便にアクセスできます。 + +たとえば、データベースがプロバイダとして登録されていれば、注入できます: + +```typescript +class Database { + //... +} + +const app = new App({ + providers: [ + Database, + ], +}); +``` + +_関数API:_ + +```typescript +router.get('/user/:id', async (id: number, database: Database) => { + return await database.query(User).filter({id}).findOne(); +}); +``` + +_コントローラAPI:_ + +```typescript +class UserController { + constructor(private database: Database) {} + + @http.GET('/user/:id') + async userDetail(id: number) { + return await this.database.query(User).filter({id}).findOne(); + } +} + +//あるいはメソッド内に直接 +class UserController { + @http.GET('/user/:id') + async userDetail(id: number, database: Database) { + return await database.query(User).filter({id}).findOne(); + } +} +``` + +詳細は [依存性注入](dependency-injection) を参照してください。 + +## スコープ + +すべてのHTTPコントローラと関数ルートは、`http` 依存性注入スコープ内で管理されます。HTTPコントローラは各HTTPリクエストごとにインスタンス化されます。これは、両者が `http` スコープに登録されたプロバイダにアクセスできることも意味します。さらに、`@deepkit/http` の `HttpRequest` と `HttpResponse` を依存関係として利用できます。deepkit フレームワークを使用している場合は、`@deepkit/framework` の `SessionHandler` も利用可能です。 + +```typescript +import { HttpResponse } from '@deepkit/http'; + +router.get('/user/:id', (id: number, request: HttpRequest) => { +}); + +router.get('/', (response: HttpResponse) => { + response.end('Hello'); +}); +``` + +各HTTPリクエストごとにサービスをインスタンス化するなどの目的で、プロバイダを `http` スコープに配置すると便利な場合があります。HTTPリクエストの処理が完了すると、`http` スコープのDIコンテナは削除され、その結果、すべてのプロバイダインスタンスはガベージコレクタ(GC)の対象となりクリーンアップされます。 + +`http` スコープにプロバイダを配置する方法については [依存性注入のスコープ](dependency-injection.md#di-scopes) を参照してください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http/events.md b/website/src/translations/ja/documentation/http/events.md new file mode 100644 index 000000000..7754af9c1 --- /dev/null +++ b/website/src/translations/ja/documentation/http/events.md @@ -0,0 +1,84 @@ +# イベント + +HTTP モジュールはワークフローエンジンに基づいており、HTTP リクエストの処理全体のプロセスにフックするために使用できる様々なイベントトークンを提供します。 + +ワークフローエンジンは有限状態機械であり、各 HTTP リクエストごとに新しい状態機械インスタンスを作成し、位置から位置へとジャンプします。最初の位置は `start`、最後は `response` です。各位置で追加のコードを実行できます。 + +![HTTP ワークフロー](/assets/documentation/framework/http-workflow.png) + +各イベントトークンには、追加情報を含む独自のイベントタイプがあります。 + +| Event-Token | 説明 | +|-------------------------------|---------------------------------------------------------------------------------------------------------------------| +| httpWorkflow.onRequest | 新しいリクエストが入ってきたとき | +| httpWorkflow.onRoute | リクエストからルートを解決する必要があるとき | +| httpWorkflow.onRouteNotFound | ルートが見つからなかったとき | +| httpWorkflow.onAuth | 認証が行われるとき | +| httpWorkflow.onResolveParameters | ルートのパラメータが解決されるとき | +| httpWorkflow.onAccessDenied | アクセスが拒否されたとき | +| httpWorkflow.onController | コントローラーのアクションが呼び出されたとき | +| httpWorkflow.onControllerError | コントローラーのアクションがエラーをスローしたとき | +| httpWorkflow.onParametersFailed | ルートのパラメータの解決に失敗したとき | +| httpWorkflow.onResponse | コントローラーのアクションが呼び出されたとき。ここで結果をレスポンスへ変換します。 | + +すべての HTTP イベントはワークフローエンジンに基づいているため、指定されたイベントを利用し、`event.next()` メソッドでそこへジャンプすることで、その挙動を変更できます。 + +HTTP モジュールは、これらのイベントトークンに対して独自のイベントリスナーを使用して HTTP リクエスト処理を実装しています。これらのイベントリスナーはすべて優先度が 100 に設定されており、あなたがイベントをリッスンするとデフォルトでは(デフォルトの優先度は 0 のため)あなたのリスナーが先に実行されます。HTTP のデフォルトハンドラーの後に実行したい場合は、優先度を 100 より大きく設定してください。 + +例えば、コントローラーが呼び出されたときのイベントを捕捉したいとします。特定のコントローラーが呼び出される場合、ユーザーがそれにアクセスできるかを確認します。ユーザーにアクセス権があればそのまま続行します。そうでなければ、次のワークフロー項目 `accessDenied` にジャンプします。そこでは、アクセス拒否の処理が自動的に継続されます。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HtmlResponse, http, httpAction, httpWorkflow } from '@deepkit/http'; +import { eventDispatcher } from '@deepkit/event'; + +class MyWebsite { + @http.GET('/') + open() { + return 'Welcome'; + } + + @http.GET('/admin').group('secret') + secret() { + return 'Welcome to the dark side'; + } +} + +const app = new App({ + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + +app.listen(httpWorkflow.onController, async (event) => { + if (event.route.groups.includes('secret')) { + //ここで Cookie セッション、JWT などの認証情報を確認します。 + + //これは 'accessDenied' ワークフロー状態にジャンプし、 + //実質的にすべての onAccessDenied リスナーを実行します。 + + //私たちのリスナーは HTTP カーネルのものより先に呼び出されるため、 + //標準のコントローラーアクションは決して呼び出されません。 + //これは内部的に event.next('accessDenied', ...) を呼び出します + event.accessDenied(); + } +}); + +/** + * 既定の accessDenied 実装を変更します。 + */ +app.listen(httpWorkflow.onAccessDenied, async () => { + if (event.sent) return; + if (event.hasNext()) return; + event.send(new HtmlResponse('No access to this area.', 403)); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Welcome +$ curl http://localhost:8080/admin +No access to this area +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http/getting-started.md b/website/src/translations/ja/documentation/http/getting-started.md new file mode 100644 index 000000000..ef7fcebde --- /dev/null +++ b/website/src/translations/ja/documentation/http/getting-started.md @@ -0,0 +1,210 @@ +# はじめに + +Deepkit HTTP は Runtime Types に基づいているため、事前に Runtime Types が正しくインストールされている必要があります。詳細は [Runtime Type のインストール](../runtime-types/getting-started.md) を参照してください。 + +これが完了していれば、`@deepkit/app` をインストールするか、すでにこのライブラリを内部で使用している Deepkit フレームワークを利用できます。 + +```sh +npm install @deepkit/http +``` + +controller API 用の `@deepkit/http` は TypeScript のアノテーションに基づいているため、controller API を使用する場合は `experimentalDecorators` を有効にする必要があります。 +Class を使用しない場合は、この機能を有効にする必要はありません。 + +_ファイル: tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +ライブラリをインストールすると、その API を直接使用できます。 + +## 関数型 API + +関数型 API は Function ベースで、アプリの DI コンテナから取得できるルーター・レジストリ経由で登録できます。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule] +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return "Hello World!"; +}); + +app.run(); +``` + +Module を使用する場合、関数型ルートは Module から動的に提供することもできます。 + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +class MyModule extends createModuleClass({}) { + override process() { + this.configureProvider<HttpRouterRegistry>(router => { + router.get('/', () => { + return "Hello World!"; + }); + }); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +App Module の詳細は、[Framework モジュール](../app/modules) を参照してください。 + +## コントローラ API + +Controller API は Class ベースで、App-API の `controllers` オプションで登録できます。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +new App({ + controllers: [MyPage], + imports: [new FrameworkModule] +}).run(); +``` + +Module を使用する場合、Controller も Module から提供できます。 + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +class MyModule extends createModuleClass({}) { + override process() { + this.addController(MyPage); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +設定オプションに応じてなど、Controller を動的に提供するには、`process` フックを使用できます。 + +```typescript +class MyModuleConfiguration { + debug: boolean = false; +} + +class MyModule extends createModuleClass({ + config: MyModuleConfiguration +}) { + override process() { + if (this.config.debug) { + class DebugController { + @http.GET('/debug/') + root() { + return 'Hello Debugger'; + } + } + this.addController(DebugController); + } + } +} +``` + +App Module の詳細は、[Framework モジュール](../app/modules) を参照してください。 + +## HTTP サーバー + +Deepkit Framework を使用する場合、HTTP サーバーはすでに内蔵されています。Deepkit フレームワークを使用せず、HTTP ライブラリを独自の HTTP サーバーと併用することもできます。 + +```typescript +import { Server } from 'http'; +import { HttpRequest, HttpResponse } from '@deepkit/http'; + +const app = new App({ + controllers: [MyPage], + imports: [new HttpModule] +}); + +const httpKernel = app.get(HttpKernel); + +new Server( + { IncomingMessage: HttpRequest, ServerResponse: HttpResponse, }, + ((req, res) => { + httpKernel.handleRequest(req as HttpRequest, res as HttpResponse); + }) +).listen(8080, () => { + console.log('listen at 8080'); +}); +``` + +## HTTP クライアント + +TODO: fetch API、バリデーション、und キャスト。 + +## ルート名 + +ルートには転送時に参照できる一意の名前を付けられます。API によって、名前の定義方法は異なります。 + +```typescript +// 関数型 API +router.get({ + path: '/user/:id', + name: 'userDetail' +}, (id: number) => { + return {userId: id}; +}); + +// コントローラ API +class UserController { + @http.GET('/user/:id').name('userDetail') + userDetail(id: number) { + return {userId: id}; + } +} +``` + +名前付きのすべてのルートから、`Router.resolveUrl()` で URL を取得できます。 + +```typescript +import { HttpRouter } from '@deepkit/http'; +const router = app.get(HttpRouter); +router.resolveUrl('userDetail', {id: 2}); //=> '/user/2' +``` + +## セキュリティ + +## セッション \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http/input-output.md b/website/src/translations/ja/documentation/http/input-output.md new file mode 100644 index 000000000..ed802e5f8 --- /dev/null +++ b/website/src/translations/ja/documentation/http/input-output.md @@ -0,0 +1,484 @@ +# 入力と出力 + +HTTP ルートの入力と出力とは、サーバーに送信されるデータおよびクライアントに返送されるデータのことです。これにはパスパラメータ、クエリパラメータ、Body、ヘッダー、そしてレスポンス自体が含まれます。本章では、HTTP ルートでデータを読み取り、逆シリアライズし、検証し、書き出す方法を見ていきます。 + +## 入力 + +以下のすべての入力バリエーションは、関数型 API とコントローラ API の両方で同じように機能します。これらは、HTTP リクエストからデータを型安全かつ疎結合な方法で読み取ることを可能にします。これによりセキュリティが大幅に向上するだけでなく、厳密にはルートをテストするのに HTTP リクエストオブジェクトすら必要ないため、ユニットテストも簡素化されます。 + +すべての Parameter は定義された TypeScript の Type に自動的に変換(逆シリアライズ)され、検証されます。これは Deepkit Runtime Types とその [シリアライゼーション](../runtime-types/serialization.md) および [バリデーション](../runtime-types/validation) 機能によって行われます。 + +簡潔さのため、以下では関数型 API を用いた例のみを示します。 + +### パスパラメータ + +パスパラメータは、ルートの URL から抽出される値です。値の型は、関数またはメソッドの対応する Parameter の型に依存します。変換は [ソフト型変換](../runtime-types/serialization#soft-type-conversion) 機能によって自動的に行われます。 + +```typescript +router.get('/:text', (text: string) => { + return 'Hello ' + text; +}); +``` + +```sh +$ curl http://localhost:8080/galaxy +Hello galaxy +``` + +パスパラメータが string 以外の Type として定義されている場合、正しく変換されます。 + +```typescript +router.get('/user/:id', (id: number) => { + return `${id} ${typeof id}`; +}); +``` + +```sh +$ curl http://localhost:8080/user/23 +23 number +``` + +追加のバリデーション制約を Type に適用することも可能です。 + +```typescript +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: number & Positive) => { + return `${id} ${typeof id}`; +}); +``` + +`@deepkit/type` のすべてのバリデーション Type を適用できます。詳細は [HTTP バリデーション](#validation) を参照してください。 + +パスパラメータは、URL マッチングの既定の正規表現として `[^]+` が設定されています。この RegExp は次のようにカスタマイズできます。 + +```typescript +import { HttpRegExp } from '@deepkit/http'; +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: HttpRegExp<number & Positive, '[0-9]+'>) => { + return `${id} ${typeof id}`; +}); +``` + +多くの場合、Type とバリデーション Type の組み合わせだけで可能な値を正しく制限できるため、これは例外的なケースでのみ必要です。 + +### クエリパラメータ + +クエリパラメータは、URL の `?` 以降の値で、`HttpQuery<T>` Type を使って読み取ることができます。Parameter 名はクエリパラメータの名前に対応します。 + +```typescript +import { HttpQuery } from '@deepkit/http'; + +router.get('/', (text: HttpQuery<number>) => { + return `Hello ${text}`; +}); +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +``` + +クエリパラメータも自動的に逆シリアライズされ、検証されます。 + +```typescript +import { HttpQuery } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/', (text: HttpQuery<string> & MinLength<3>) => { + return 'Hello ' + text; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +$ curl http://localhost:8080/\?text\=ga +error +``` + +`@deepkit/type` のすべてのバリデーション Type を適用できます。詳細は [HTTP バリデーション](#validation) を参照してください。 + +警告: Parameter の値はエスケープ/サニタイズされません。ルートで文字列としてそのまま HTML に返すと、セキュリティホール(XSS)になります。外部入力を決して信用せず、必要に応じてフィルタ/サニタイズ/変換を行ってください。 + +### クエリモデル + +クエリパラメータが多くなるとすぐに混乱しがちです。これを整理するために、すべてのクエリパラメータをまとめるモデル(Class または Interface)を使用できます。 + +```typescript +import { HttpQueries } from '@deepkit/http'; + +class HelloWorldQuery { + text!: string; + page: number = 0; +} + +router.get('/', (query: HttpQueries<HelloWorldQuery>) +{ + return 'Hello ' + query.text + ' at page ' + query.page; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy&page=1 +Hello galaxy at page 1 +``` + +指定されたモデル内の Property には、`@deepkit/type` がサポートするあらゆる TypeScript の Type およびバリデーション Type を含めることができます。詳しくは [シリアライゼーション](../runtime-types/serialization.md) と [バリデーション](../runtime-types/validation.md) の章を参照してください。 + +### Body + +HTTP Body を許可する HTTP メソッドでは、Body モデルを指定することもできます。HTTP リクエストの Body の Content-Type は `application/x-www-form-urlencoded`、`multipart/form-data`、または `application/json` のいずれかである必要があり、Deepkit はこれを自動的に JavaScript オブジェクトへ変換できます。 + +```typescript +import { HttpBody } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBody<HelloWorldBody>) => { + return 'Hello ' + body.text; +} +``` + +### ヘッダー + +### ストリーム + +### バリデーションの手動処理 + +Body モデルの検証を手動で引き受けるには、特別な Type `HttpBodyValidation<T>` が使用できます。これにより、不正な Body データも受け取り、エラーメッセージに非常に具体的に対応できます。 + +```typescript +import { HttpBodyValidation } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBodyValidation<HelloWorldBody>) => { + if (!body.valid()) { + // ヒューストン、いくつかエラーがあります。 + const textError = body.getErrorMessageForPath('text'); + return 'Text is invalid, please fix it. ' + textError; + } + + return 'Hello ' + body.text; +}) +``` + +`valid()` が `false` を返した時点で、指定されたモデル内の値は不正な状態にある可能性があります。これはバリデーションに失敗したことを意味します。`HttpBodyValidation` を使用せずに誤った HTTP リクエストが受信された場合、リクエストは直接中断され、関数内のコードは実行されません。Body に関するエラーメッセージを同じルート内で手動処理したい場合などにのみ `HttpBodyValidation` を使用してください。 + +指定されたモデル内の Property には、`@deepkit/type` がサポートするあらゆる TypeScript の Type およびバリデーション Type を含めることができます。詳しくは [シリアライゼーション](../runtime-types/serialization.md) と [バリデーション](../runtime-types/validation.md) の章を参照してください。 + +### ファイルアップロード + +クライアントにファイルのアップロードを許可するために、Body モデルに特別な Property Type を使用できます。任意の数の `UploadedFile` を使用できます。 + +```typescript +import { UploadedFile, HttpBody } from '@deepkit/http'; +import { readFileSync } from 'fs'; + +class HelloWordBody { + file!: UploadedFile; +} + +router.post('/', (body: HttpBody<HelloWordBody>) => { + const content = readFileSync(body.file.path); + + return { + uploadedFile: body.file + }; +}) +``` + +```sh +$ curl http://localhost:8080/ -X POST -H "Content-Type: multipart/form-data" -F "file=@Downloads/23931.png" +{ + "uploadedFile": { + "size":6430, + "path":"/var/folders/pn/40jxd3dj0fg957gqv_nhz5dw0000gn/T/upload_dd0c7241133326bf6afddc233e34affa", + "name":"23931.png", + "type":"image/png", + "lastModifiedDate":"2021-06-11T19:19:14.775Z" + } +} +``` + +既定では、Router はアップロードされたファイルをすべて一時フォルダに保存し、ルート内のコードが実行された後に削除します。そのため、`path` に指定されたパスでファイルを読み込み、永続的な場所(ローカルディスク、クラウドストレージ、データベース)に保存する必要があります。 + +## バリデーション + +HTTP サーバーでのバリデーションは必須の機能です。というのも、ほぼ常に信用できないデータを扱うからです。データが検証される箇所が多いほど、サーバーはより安定します。HTTP ルートでのバリデーションは Type とバリデーション制約を通じて簡便に利用でき、`@deepkit/type` の高度に最適化されたバリデータでチェックされるため、パフォーマンス上の問題はありません。したがって、これらのバリデーション機能を積極的に使用することを強く推奨します。やり過ぎなくらいが、やり足りないより良いのです。 + +パスパラメータ、クエリパラメータ、Body パラメータなど、すべての入力は指定された TypeScript の Type に対して自動的に検証されます。`@deepkit/type` の Type を通じて追加の制約が指定されている場合、それらもチェックされます。 + +```typescript +import { HttpQuery, HttpQueries, HttpBody } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/:text', (text: string & MinLength<3>) => { + return 'Hello ' + text; +} + +router.get('/', (text: HttpQuery<string> & MinLength<3>) => { + return 'Hello ' + text; +} + +interface MyQuery { + text: string & MinLength<3>; +} + +router.get('/', (query: HttpQueries<MyQuery>) => { + return 'Hello ' + query.text; +}); + +router.post('/', (body: HttpBody<MyQuery>) => { + return 'Hello ' + body.text; +}); +``` + +詳しくは [バリデーション](../runtime-types/validation.md) を参照してください。 + +## 出力 + +ルートはさまざまなデータ構造を返すことができます。リダイレクトやテンプレートなど特別に扱われるものもあれば、単純なオブジェクトのように JSON としてそのまま送信されるものもあります。 + +### JSON + +既定では、通常の JavaScript の値はヘッダー `application/json; charset=utf-8` とともに JSON としてクライアントに返されます。 + +```typescript +router.get('/', () => { + // application/json で送信されます + return { hello: 'world' } +}); +``` + +関数またはメソッドに明示的な Return Type が指定されている場合、データはその Type に従って Deepkit JSON Serializer で JSON にシリアライズされます。 + +```typescript +interface ResultType { + hello: string; +} + +router.get('/', (): ResultType => { + // application/json で送信され、additionalProperty は破棄されます + return { hello: 'world', additionalProperty: 'value' }; +}); +``` + +### HTML + +HTML を送信するには 2 つの方法があります。`HtmlResponse` オブジェクトを使用するか、JSX を用いたテンプレートエンジンを使用します。 + +```typescript +import { HtmlResponse } from '@deepkit/http'; + +router.get('/', () => { + // Content-Type: text/html で送信されます + return new HtmlResponse('<b>Hello World</b>'); +}); +``` + +```typescript +router.get('/', () => { + // Content-Type: text/html で送信されます + return <b>Hello + World < /b>; +}); +``` + +JSX を用いたテンプレートエンジンのバリアントには、使用される変数が自動的に HTML エスケープされるという利点があります。詳しくは [テンプレート](./template.md) を参照してください。 + +### カスタム Content-Type + +HTML と JSON 以外にも、特定の Content-Type でテキストまたはバイナリデータを送信することができます。これは `Response` オブジェクトを通じて行います。 + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('<title>Hello World', 'text/xml'); +}); +``` + +### HTTP エラー + +さまざまな HTTP エラーを投げることで、HTTP リクエストの処理を即座に中断し、そのエラーに対応する HTTP ステータスを出力できます。 + +```typescript +import { HttpNotFoundError } from '@deepkit/http'; + +router.get('/user/:id', async (id: number, database: Database) => { + const user = await database.query(User).filter({ id }).findOneOrUndefined(); + if (!user) throw new HttpNotFoundError('User not found'); + return user; +}); +``` + +既定では、すべてのエラーは JSON としてクライアントに返されます。この挙動はイベントシステムの `httpWorkflow.onControllerError` イベントでカスタマイズできます。セクション [HTTP イベント](./events.md) を参照してください。 + +| Error クラス | ステータス | +|---------------------------|------------| +| HttpBadRequestError | 400 | +| HttpUnauthorizedError | 401 | +| HttpAccessDeniedError | 403 | +| HttpNotFoundError | 404 | +| HttpMethodNotAllowedError | 405 | +| HttpNotAcceptableError | 406 | +| HttpTimeoutError | 408 | +| HttpConflictError | 409 | +| HttpGoneError | 410 | +| HttpTooManyRequestsError | 429 | +| HttpInternalServerError | 500 | +| HttpNotImplementedError | 501 | + +`HttpAccessDeniedError` は特別なケースです。これが投げられると、HTTP ワークフロー([HTTP イベント](./events.md) 参照)は `controllerError` ではなく `accessDenied` に移行します。 + +`createHttpError` を使ってカスタム HTTP エラーを作成してスローできます。 + +```typescript +export class HttpMyError extends createHttpError(412, 'My Error Message') { +} +``` + +コントローラアクションでスローされたエラーは、HTTP ワークフローイベント `onControllerError` によって処理されます。既定の実装では、エラーメッセージとステータスコードを含む JSON レスポンスを返します。これはこのイベントをリッスンし、別のレスポンスを返すことでカスタマイズできます。 + +```typescript +import { httpWorkflow } from '@deepkit/http'; + +new App() + .listen(httpWorkflow.onControllerError, (event) => { + if (event.error instanceof HttpMyError) { + event.send(new Response('My Error Message', 'text/plain').status(500)); + } else { + // 他のすべてのエラーについては、汎用的なエラーメッセージを返します + event.send(new Response('Something went wrong. Sorry about that.', 'text/plain').status(500)); + } + }) + .listen(httpWorkflow.onAccessDenied, (event) => { + event.send(new Response('Access denied. Try to login first.', 'text/plain').status(403)); + }); +``` + +### 追加ヘッダー + +HTTP レスポンスのヘッダーを変更するには、`Response`、`JSONResponse`、`HTMLResponse` オブジェクト上で追加のメソッドを呼び出せます。 + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('Access Denied', 'text/plain') + .header('X-Reason', 'unknown') + .status(403); +}); +``` + +### リダイレクト + +レスポンスとして 301 または 302 のリダイレクトを返すには、`Redirect.toRoute` または `Redirect.toUrl` を使用します。 + +```typescript +import { Redirect } from '@deepkit/http'; + +router.get({ path: '/', name: 'homepage' }, () => { + return Hello + World < /b>; +}); + +router.get({ path: '/registration/complete' }, () => { + return Redirect.toRoute('homepage'); +}); +``` + +`Redirect.toRoute` メソッドはここでルート名を使用します。ルート名の設定方法は [HTTP ルート名](./getting-started.md#route-names) セクションを参照してください。参照されたルート(クエリまたはパス)に Parameter が含まれる場合は、第 2 引数で指定できます。 + +```typescript +router.get({ path: '/user/:id', name: 'user_detail' }, (id: number) => { + +}); + +router.post('/user', (user: HttpBody) => { + //... ユーザーを保存して詳細ページにリダイレクト + return Redirect.toRoute('user_detail', { id: 23 }); +}); +``` + +`Redirect.toUrl` を使って URL にリダイレクトすることもできます。 + +```typescript +router.post('/user', (user: HttpBody) => { + //... ユーザーを保存して詳細ページにリダイレクト + return Redirect.toUrl('/user/' + 23); +}); +``` + +既定では、どちらも 302 リダイレクトを使用します。これは `statusCode` 引数でカスタマイズできます。 + +## リゾルバ + +Router は、複雑な Parameter Type を解決する方法をサポートしています。たとえば、`/user/:id` のようなルートが与えられた場合、この `id` をリゾルバを使ってルートの外で `user` オブジェクトに解決できます。これにより HTTP 抽象化とルートコードがさらに疎結合になり、テストやモジュール性が一層簡素化されます。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http, RouteParameterResolverContext, RouteParameterResolver } from '@deepkit/http'; + +class UserResolver implements RouteParameterResolver { + constructor(protected database: Database) { + } + + async resolve(context: RouteParameterResolverContext) { + if (!context.parameters.id) throw new Error('No :id given'); + return await this.database.getUser(parseInt(context.parameters.id, 10)); + } +} + +@http.resolveParameter(User, UserResolver) +class MyWebsite { + @http.GET('/user/:id') + getUser(user: User) { + return 'Hello ' + user.username; + } +} + +new App({ + controllers: [MyWebsite], + providers: [UserDatabase, UserResolver], + imports: [new FrameworkModule] +}) + .run(); +``` + +`@http.resolveParameter` のデコレータは、どの Class を `UserResolver` で解決するかを指定します。指定された Class `User` が関数またはメソッドの Parameter として指定されるとすぐに、リゾルバが使用されてそれが提供されます。 + +`@http.resolveParameter` が Class に指定されている場合、その Class のすべての Method にこのリゾルバが適用されます。デコレータはメソッド単位でも適用できます。 + +```typescript +class MyWebsite { + @http.GET('/user/:id').resolveParameter(User, UserResolver) + getUser(user: User) { + return 'Hello ' + user.username; + } +} +``` + +関数型 API も使用できます。 + +```typescript + +router.add( + http.GET('/user/:id').resolveParameter(User, UserResolver), + (user: User) => { + return 'Hello ' + user.username; + } +); +``` + +`User` オブジェクトは必ずしも Parameter に依存する必要はありません。セッションや HTTP ヘッダーに依存し、ユーザーがログインしている場合にのみ提供されるようにしても構いません。`RouteParameterResolverContext` には HTTP リクエストに関する多くの情報が用意されているため、多くのユースケースを表現できます。 + +原則として、複雑な Parameter Type を `http` スコープの Dependency Injection コンテナ経由で提供することも可能です。これは、ルートの関数またはメソッドでも利用できるためです。ただし、DI コンテナは全体として同期的であるため、非同期関数呼び出しを使用できないという欠点があります。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http/middleware.md b/website/src/translations/ja/documentation/http/middleware.md new file mode 100644 index 000000000..3e1d61f21 --- /dev/null +++ b/website/src/translations/ja/documentation/http/middleware.md @@ -0,0 +1,261 @@ +# ミドルウェア + +HTTP ミドルウェアは、HTTP events の代替として request/response サイクルにフックすることを可能にします。その API により、Express/Connect フレームワークのあらゆるミドルウェアを利用できます。 + +ミドルウェアは、依存性注入コンテナによってインスタンス化される Class か、単純な Function のいずれかです。 + +```typescript +import { HttpMiddleware, httpMiddleware, HttpRequest, HttpResponse } from '@deepkit/http'; + +class MyMiddleware implements HttpMiddleware { + async execute(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); + } +} + + +function myMiddlewareFunction(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); +} + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware), + httpMiddleware.for(myMiddlewareFunction), + ], + imports: [new FrameworkModule] +}).run(); +``` + +## グローバル + +httpMiddleware.for(MyMiddleware) を使用すると、ミドルウェアは全ての Route に対してグローバルに登録されます。 + +```typescript +import { httpMiddleware } from '@deepkit/http'; + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Controller ごと + +1 つまたは複数の Controller にミドルウェアを制限する方法は 2 つあります。`@http.controller` を使うか、`httpMiddleware.for(T).forControllers()` を使います。`excludeControllers` を使うと Controller を除外できます。 + +```typescript +@http.middleware(MyMiddleware) +class MyFirstController { + +} +new App({ + providers: [MyMiddleware], + controllers: [MainController, UsersCommand], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyFirstController, MySecondController) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Route 名ごと + +`forRouteNames` と、その対になる `excludeRouteNames` により、Route 名ごとにミドルウェアの実行をフィルタできます。 + +```typescript +class MyFirstController { + @http.GET('/hello').name('firstRoute') + myAction() { + } + + @http.GET('/second').name('secondRoute') + myAction2() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRouteNames('firstRoute', 'secondRoute') + ], + imports: [new FrameworkModule] +}).run(); +``` + +## Action/Route ごと + +特定の Route のみにミドルウェアを適用するには、`@http.GET().middleware()` を使うか、 +`httpMiddleware.for(T).forRoute()` を使います。forRoute には Route をフィルタするための複数のオプションがあります。 + +```typescript +class MyFirstController { + @http.GET('/hello').middleware(MyMiddleware) + myAction() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' + }) + ], + imports: [new FrameworkModule] +}).run(); +``` + +`forRoutes()` は、最初の引数で Route をフィルタするための複数の方法を指定できます。 + +```typescript +{ + path?: string; + pathRegExp?: RegExp; + httpMethod?: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS' | 'TRACE'; + category?: string; + excludeCategory?: string; + group?: string; + excludeGroup?: string; +} +``` + +## パスパターン + +`path` はワイルドカード * をサポートします。 + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' +}) +``` + +## RegExp + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + pathRegExp: /'api/.*'/ +}) +``` + +## HTTP Method + +HTTP Method で全ての Route をフィルタします。 + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + httpMethod: 'GET' +}) +``` + +## カテゴリ + +`category` と、その対になる `excludeCategory` により、Route のカテゴリごとにフィルタできます。 + +```typescript +@http.category('myCategory') +class MyFirstController { + +} + +class MySecondController { + @http.GET().category('myCategory') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + category: 'myCategory' +}) +``` + +## グループ + +`group` と、その対になる `excludeGroup` により、Route のグループごとにフィルタできます。 + +```typescript +@http.group('myGroup') +class MyFirstController { + +} + +class MySecondController { + @http.GET().group('myGroup') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + group: 'myGroup' +}) +``` + +## Module ごと + +ミドルウェアの実行を特定の Module 全体に限定できます。 + +```typescript +httpMiddleware.for(MyMiddleware).forModule(ApiModule) +``` + +## 自身の Module ごと + +ミドルウェアが登録された Module 内のすべての Controller/Route に対してミドルウェアを実行するには、`forSelfModules()` を使用します。 + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //同じ Module に登録されたすべての Controller に対して + httpMiddleware.for(MyMiddleware).forSelfModules(), + ], +}); +``` + +## タイムアウト + +すべてのミドルウェアは遅かれ早かれ `next()` を実行する必要があります。タイムアウト内にミドルウェアが `next()` を実行しない場合、警告がログに記録され、次のミドルウェアが実行されます。デフォルトの 4 秒を変更するには、`timeout(milliseconds)` を使用します。 + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //同じ Module に登録されたすべての Controller に対して + httpMiddleware.for(MyMiddleware).timeout(15_000), + ], +}); +``` + +## 複数のルール + +複数のフィルタを組み合わせるには、Method 呼び出しをチェーンできます。 + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MyController], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyController).excludeRouteNames('secondRoute') + ], +}); +``` + +## Express ミドルウェア + +ほとんどの Express ミドルウェアはサポートされています。Express の特定の request メソッドにアクセスするものは、まだサポートされていません。 + +```typescript +import * as compression from 'compression'; + +const ApiModule = new AppModule({}, { + middlewares: [ + httpMiddleware.for(compress()).forControllers(MyController) + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http/security.md b/website/src/translations/ja/documentation/http/security.md new file mode 100644 index 000000000..025b99851 --- /dev/null +++ b/website/src/translations/ja/documentation/http/security.md @@ -0,0 +1,3 @@ +# セキュリティ + +このセクションはまだ作成中です。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/http/views.md b/website/src/translations/ja/documentation/http/views.md new file mode 100644 index 000000000..4a5256739 --- /dev/null +++ b/website/src/translations/ja/documentation/http/views.md @@ -0,0 +1,32 @@ +# HTML ビュー + +Deepkit HTTP には組み込みの HTML ビュー描画システムが付属しています。これは JSX を基盤としており、ビューを TypeScript で記述できます。独自の構文を持つテンプレートエンジンではなく、完全な TypeScript/JSX レンダラーです。 + +実行時に JSX コードを最適化し、結果をキャッシュします。そのため非常に高速で、ほとんどオーバーヘッドがありません。 + + +## JSX + +JSX は JavaScript の構文拡張で、TypeScript を標準でサポートしています。TypeScript で HTML を記述できます。Vue.js や React.js に非常によく似ています。 + +```tsx app=app.ts +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from "@deepkit/http"; + +export function View() { + return
+

Hello World

+

My first JSX view

+
; +} + +const app = new App({}); +const router = app.get(HttpRouterRegistry); + +router.get('/', () => ); + +app.run(); +``` + +```sh +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/index.md b/website/src/translations/ja/documentation/index.md new file mode 100644 index 000000000..86fc32974 --- /dev/null +++ b/website/src/translations/ja/documentation/index.md @@ -0,0 +1,87 @@ +# ドキュメント + +Deepkit は、MIT ライセンスの下で自由に利用できるオープンソースの TypeScript フレームワークで、スケーラブルで保守しやすいバックエンドアプリケーションの構築を支援します。ブラウザと Node.js で動作するように設計されていますが、適切な JavaScript 環境であればどこでも実行できます。 + +ここでは Deepkit のさまざまなコンポーネントに関する章と、すべてのパッケージの API リファレンスを見つけることができます。 + +ヘルプが必要な場合は、[Discord サーバー](https://discord.com/invite/PtfVf7B8UU) に参加するか、[GitHub](https://github.com/deepkit/deepkit-framework) で issue を作成してください。 + +## 章 + + +- [アプリ](/documentation/app.md) - コマンドラインインターフェイス (CLI) を使って Deepkit で最初のアプリケーションを作成します。 +- [フレームワーク](/documentation/framework.md) - アプリケーションに (HTTP/RPC) サーバー、API ドキュメント、デバッガ、統合テストなどを追加します。 +- [ランタイム型](/documentation/runtime-types.md) - TypeScript のランタイム型について学び、データの検証と変換を行います。 +- [依存性注入](/documentation/dependency-injection.md) - 依存性注入コンテナ、制御の反転、依存性逆転。 +- [ファイルシステム](/documentation/filesystem.md) - ローカルおよびリモートのファイルシステムを統一的に扱うためのファイルシステム抽象化。 +- [ブローカー](/documentation/broker.md) - 分散 L2 キャッシュ、Pub/Sub、キュー、中央のアトミックロック、キー・バリューストアを扱うためのメッセージブローカー抽象化。 +- [HTTP](/documentation/http.md) - 型安全なエンドポイントを構築するための HTTP サーバー抽象化。 +- [RPC](/documentation/rpc.md) - フロントエンドとバックエンド、または複数のバックエンドサービスを接続するためのリモートプロシージャコール (RPC) 抽象化。 +- [ORM](/documentation/orm.md) - 型安全にデータを保存およびクエリするための ORM と DBAL。 +- [デスクトップ UI](/documentation/desktop-ui/getting-started) - Deepkit の Angular ベースの UI フレームワークで GUI アプリケーションを構築します。 + +## API リファレンス + +以下は、すべての Deepkit パッケージとその API ドキュメントへのリンクの完全な一覧です。 + +### 構成 + +- [@deepkit/app](/documentation/package/app.md) +- [@deepkit/framework](/documentation/package/framework.md) +- [@deepkit/http](/documentation/package/http.md) +- [@deepkit/angular-ssr](/documentation/package/angular-ssr.md) + +### インフラストラクチャ + +- [@deepkit/rpc](/documentation/package/rpc.md) +- [@deepkit/rpc-tcp](/documentation/package/rpc-tcp.md) +- [@deepkit/broker](/documentation/package/broker.md) +- [@deepkit/broker-redis](/documentation/package/broker-redis.md) + +### ファイルシステム + +- [@deepkit/filesystem](/documentation/package/filesystem.md) +- [@deepkit/filesystem-ftp](/documentation/package/filesystem-ftp.md) +- [@deepkit/filesystem-sftp](/documentation/package/filesystem-sftp.md) +- [@deepkit/filesystem-s3](/documentation/package/filesystem-s3.md) +- [@deepkit/filesystem-google](/documentation/package/filesystem-google.md) +- [@deepkit/filesystem-database](/documentation/package/filesystem-database.md) + +### データベース + +- [@deepkit/orm](/documentation/package/orm.md) +- [@deepkit/mysql](/documentation/package/mysql.md) +- [@deepkit/postgres](/documentation/package/postgres.md) +- [@deepkit/sqlite](/documentation/package/sqlite.md) +- [@deepkit/mongodb](/documentation/package/mongodb.md) + +### 基礎 + +- [@deepkit/type](/documentation/package/type.md) +- [@deepkit/event](/documentation/package/event.md) +- [@deepkit/injector](/documentation/package/injector.md) +- [@deepkit/template](/documentation/package/template.md) +- [@deepkit/logger](/documentation/package/logger.md) +- [@deepkit/workflow](/documentation/package/workflow.md) +- [@deepkit/stopwatch](/documentation/package/stopwatch.md) + +### ツール + +- [@deepkit/api-console](/documentation/package/api-console.md) +- [@deepkit/devtool](/documentation/package/devtool.md) +- [@deepkit/desktop-ui](/documentation/package/desktop-ui.md) +- [@deepkit/orm-browser](/documentation/package/orm-browser.md) +- [@deepkit/bench](/documentation/package/bench.md) +- [@deepkit/run](/documentation/package/run.md) + +### コア + +- [@deepkit/bson](/documentation/package/bson.md) +- [@deepkit/core](/documentation/package/core.md) +- [@deepkit/topsort](/documentation/package/topsort.md) + +### ランタイム + +- [@deepkit/vite](/documentation/package/vite.md) +- [@deepkit/bun](/documentation/package/bun.md) +- [@deepkit/type-compiler](/documentation/package/type-compiler.md) \ No newline at end of file diff --git a/website/src/translations/ja/documentation/introduction.md b/website/src/translations/ja/documentation/introduction.md new file mode 100644 index 000000000..631560f1a --- /dev/null +++ b/website/src/translations/ja/documentation/introduction.md @@ -0,0 +1,45 @@ +# はじめに + +TypeScript は JavaScript の高いスケーラビリティを備えたスーパーセットとして登場し、より安全で堅牢なアプリケーションの開発を可能にするよう設計されています。JavaScript は多大な開発者コミュニティとエコシステムを築いてきましたが、TypeScript は静的型付けの力を JavaScript にもたらし、ランタイムエラーを大幅に減らし、コードベースの保守性と可読性を高めます。しかし、その利点にもかかわらず、特に複雑なエンタープライズレベルのソリューションの実装において、TypeScript の可能性は十分に活用されていません。TypeScript は本質的にコンパイル時に型情報を消去してしまうため、ランタイム機能に重要なギャップが生じ、型情報を保持するために各種の扱いづらいワークアラウンドを実装せざるを得ません。コード生成、制限のあるデコレーター、Zod のように複雑な推論ステップを伴うカスタム型ビルダーなど、これらの解決策はどれも煩雑で遅く、エラーが発生しやすいものです。その結果、開発速度が低下するだけでなく、特に大規模チームや複雑なプロジェクトではアプリケーションの堅牢性も損なわれます。 + +そこで登場するのが Deepkit です。TypeScript を用いて複雑で効率的なソフトウェアソリューションを構築する方法を革新するフレームワークです。TypeScript で設計され、TypeScript のために作られた Deepkit は、開発中の型安全性を実現するだけでなく、その恩恵をランタイムにまで拡張します。ランタイムで型情報を保持することで、動的な型計算、データ検証、シリアライゼーションなど、従来は実装が煩雑だった数多くの新機能への道を切り開きます。 + +Deepkit は高い複雑性のプロジェクトやエンタープライズレベルのアプリケーションに対応するよう設計されていますが、その俊敏性とモジュール型アーキテクチャにより、小規模アプリケーションにも同様に適しています。豊富なライブラリ群が一般的なユースケースを網羅しており、プロジェクトの要件に応じて個別にも統合的にも利用できます。Deepkit は必要なだけ柔軟に、求められるだけ構造化された形を目指し、短期・長期のいずれにおいても高い開発速度を維持できるようにします。 + +## なぜ Deepkit なのか? + +TypeScript のエコシステムには無数のライブラリやツールが存在し、ほぼあらゆる問題に対する解決策が提供されています。この豊富さは力を与える一方で、ライブラリ間の思想、API、コード品質の不一致により複雑さを招くことがよくあります。これらの異種コンポーネントを統合するには追加の抽象化が必要で、多くの場合グルーコードが大量に発生し、保守の悪夢となってすぐに手に負えなくなり、開発速度を著しく低下させます。Deepkit は統合フレームワークを提供することでこれらの課題を緩和することを目指し、ほぼすべてのプロジェクトに必要な中核機能をひとつにまとめます。調和の取れたライブラリとコンポーネントのセットはシームレスに連携するよう設計されており、断片化した TypeScript エコシステムに存在するギャップを埋めます。 + +### 実績あるエンタープライズの原則 + +Deepkit は、Java の Spring や PHP の Laravel、Symfony といった確立されたエンタープライズフレームワークから着想を得ています。これらのフレームワークは数十年にわたりその効率性と堅牢性が実証され、無数の成功プロジェクトの礎となってきました。Deepkit は同様のエンタープライズの設計パターンや概念を新しく独自のやり方で TypeScript の世界にもたらし、長年にわたる集合知の恩恵を開発者が享受できるようにします。 + +これらのパターンはアプリケーションを構造化する実証済みの方法を提供するだけでなく、特に大規模チームでの開発を円滑にします。こうした実績ある方法論を活用することで、TypeScript の領域では得がたかった信頼性とスケーラビリティの水準を提供することを目指します。 + +### アジャイル / 長期的なパフォーマンス + +Deepkit は俊敏性を念頭に設計されており、初期開発を加速し、長期的な保守にも利点をもたらすツールと機能を提供します。将来の拡張性を犠牲にして初速を優先するフレームワークもある中、Deepkit はその両立を図ります。設計パターンは理解しやすく、導入は容易ですが、同時に効果的にスケールし、プロジェクトやチームが拡大しても開発速度が落ちないことを保証します。 + +この先見的なアプローチにより、Deepkit は迅速な MVP にも、複雑で長寿命のエンタープライズアプリケーションにも最適な選択となります。 + +### 開発者体験 + +最後に、Deepkit は開発者体験を強く重視しています。フレームワークは直感的な API、詳細なドキュメント、支援的なコミュニティを提供し、開発者が技術的な複雑さと格闘するのではなくビジネス課題の解決に集中できるようにします。小規模アプリケーションから大規模なエンタープライズ級システムまで、Deepkit はスムーズで実りある開発体験を実現するためのツールとプラクティスを提供します。 + +## 主要な機能 + +### ランタイム型 + +Deepkit の際立った機能のひとつは、ランタイムにおいて型情報を保持できることです。多くの従来の TypeScript フレームワークはコンパイル過程でこの重要なデータを破棄してしまい、データ検証、シリアライゼーション、依存性注入といったランタイムの処理をはるかに面倒にします。Deepkit の型コンパイラは、ランタイムでの動的な型計算や既存の型情報の読み取りを独自に可能にします。これにより柔軟性が高まるだけでなく、より堅牢で型安全なアプリケーションを実現し、複雑なシステムの開発を効率化します。 + +### 包括的なライブラリスイート + +Deepkit はアプリケーション開発のさまざまな側面を加速するために設計された、完全なエコシステムのライブラリを提供します。データベース抽象化や CLI パーサーから、HTTP ルーターや RPC フレームワークまで、Deepkit のライブラリは多様な開発ニーズに応える統一的な解決策を提供します。これらすべてのライブラリは、ランタイムで TypeScript の型システムを活用できるという利点を備えており、ボイラープレートを大幅に削減し、コードの明瞭性を高めます。Deepkit のモジュール性により、開発者は特定のタスクに個別のライブラリを使うことも、フレームワーク全体を採用して本番運用に耐えるアプリケーションを構築することもできます。 + +## 高パフォーマンスとスケーラビリティ + +プロジェクトの複雑さが増す中で開発速度を維持することは大きな課題です。Deepkit は、より大規模なチームや複雑なコードベースでもうまくスケールする、実証済みのエンタープライズ設計パターンの適用を重視することで、この問題に正面から取り組みます。このフレームワークは、より大規模なチームや複雑なコードベースでうまくスケールすることが実証されている確立されたエンタープライズ設計パターンを取り入れています。Deepkit のアプローチは、初期段階にとどまらずライフサイクル全体を通じて、プロジェクトが俊敏かつ効率的であり続けることを保証します。これは、ボイラープレートを最小化し、可能な限り扱いやすい形で設計パターンを活用することで実現され、チームが長期にわたり高い生産性を維持できるようにします。 + +### アイソモーフィック TypeScript + +Deepkit は、フロントエンド、バックエンド、さらにはモバイルアプリケーションに至るまで、同一のコードベースを複数プラットフォームで利用できるアイソモーフィック TypeScript の利点を最大化するよう設計されています。これにより、コードを各部門間で共有できるため、時間とコストを大幅に節約でき、採用活動が容易になり、チーム内での知識移転も促進されます。Deepkit はアイソモーフィック TypeScript の力を余すところなく活用し、従来のデュアルスタック手法を大きく凌駕する、統合されたクロスプラットフォーム開発体験を提供します。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm.md b/website/src/translations/ja/documentation/orm.md new file mode 100644 index 000000000..1a343ec3c --- /dev/null +++ b/website/src/translations/ja/documentation/orm.md @@ -0,0 +1,15 @@ +# Deepkit ORM + +Deepkit ORM は高性能な TypeScript の ORM(オブジェクト関係マッパー)です。データベースとやり取りするためのシンプルで直感的な API を提供し、低レベルなデータベース操作を気にするのではなくアプリケーションの構築に集中できるようにします。ORM は Deepkit の Runtime Type System の上に構築されており、データベース操作のための型安全な環境を提供します。 + +## なぜ ORM なのか? + +Deepkit の Object-Relational Mapping(ORM)は、開発者にいくつかの利点をもたらします。 + +1. データベース操作の簡素化: ORM を使用すると、開発者は SQL クエリの手動作成と実行を抽象化できます。代わりに、より直感的なオブジェクト指向のアプローチでデータベースとやり取りできます。これにより、クエリ実行、挿入、更新、削除といった一般的なデータベース操作が簡素化されます。 + +2. データベース間の互換性: ORM は、さまざまなデータベースシステムとやり取りするための一貫した API を提供することで、開発者がデータベース非依存のコードを記述できるようにします。つまり、コードベースに大きな変更を加えることなく、MySQL、PostgreSQL、SQLite などの異なるデータベースエンジン間を容易に切り替えられます。 + +3. 型安全性とコンパイル時チェック: ランタイムの型情報を活用することで、Deepkit の ORM はデータベース操作のための型安全な環境を提供します。ORM を使用すれば、データベーススキーマを TypeScript の Class や Interface として定義でき、実行時ではなくコンパイル時に潜在的なエラーを捕捉できます。さらに、ORM は型の自動変換とバリデーションを処理し、データが常に一貫性を保ち、データベースに正しく永続化されることを保証します。 + +総合的に見ると、Deepkit で ORM を使用することは、データベース操作を簡素化し、データベース間の互換性を高め、型安全性とコンパイル時チェックを提供するため、堅牢で保守しやすいアプリケーションを構築するうえで不可欠なコンポーネントとなります。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/composite-primary-key.md b/website/src/translations/ja/documentation/orm/composite-primary-key.md new file mode 100644 index 000000000..cb79d4639 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/composite-primary-key.md @@ -0,0 +1,84 @@ +# Composite Primary Key + +Composite Primary Key とは、ある Entity が複数の Primary Key を持ち、それらが自動的に組み合わさって「Composite Primary Key」になることを意味します。こうした database のモデリングには長所と短所があります。私たちは、Composite Primary Key は利点を正当化できないほど実務上の大きな欠点を伴うと考えており、bad practice とみなし、避けるべきだと考えます。Deepkit ORM は Composite Primary Key をサポートしていません。本章ではその理由を説明し、(より良い)代替案を示します。 + +## 欠点 + +Join は自明ではありません。RDBMS で高度に最適化されているとはいえ、アプリケーション側では一定の複雑性として存在し、すぐに手に負えなくなってパフォーマンス問題を引き起こしかねません。パフォーマンスとはクエリの実行時間だけでなく、開発時間の観点でも同様です。 + +## Joins + +各 Join は、関与する field が増えるほど複雑になります。多くの database は複数 field の Join を必ずしも遅くしない最適化を実装していますが、開発者は常にその Join を細部まで考慮する必要があります。たとえば key を一つ忘れると(すべての key を指定しなくても Join 自体は動作するため)微妙な誤りにつながり得るので、開発者は Composite Primary Key の完全な構造を把握しておく必要があります。 + +## Indizes + +複数 field を持つ Index(すなわち Composite Primary Key)は、クエリ内での field の順序という問題に悩まされます。database system は特定のクエリを最適化できますが、構造が複雑になると、定義済みのすべての Index を正しく活用する効率的な操作を書くことが難しくなります。複数 field の Index(Composite Primary Key のような)では、database が実際にその Index を使うために、通常は field を正しい順序で定義する必要があります。順序が正しく指定されない場合(たとえば WHERE 句で)、database が Index をまったく使わず、代わりに full table scan を行ってしまうことは容易に起こります。どの database クエリがどのように最適化されるかを知るのは高度な知識であり、通常は新しい開発者が持っているものではありません。しかし、Composite Primary Key を使い始めた時点で、database を最大限に活用し無駄なリソース消費を避けるには、その知識が必要になります。 + +## Migrationen + +一度、特定の Entity を一意に識別するために追加の field が必要だと判断すると(その結果、それが Composite Primary Key になると)、その Entity と関係を持つ database 内のすべての Entity を調整する必要が生じます。 + +たとえば、Composite Primary Key を持つ `user` という Entity があり、さまざまな table でこの `user` への foreign key を使うことにしたとします。例として pivot table の `audit_log`、`groups`、`posts` などです。`user` の Primary Key を変更すると、これらすべての table も Migration で調整する必要があります。 + +これは Migration ファイルをはるかに複雑にするだけでなく、Migration 実行時の大きなダウンタイムを引き起こす可能性もあります。というのも schema の変更には、通常、database 全体の lock か少なくとも table lock が必要になるためです。Index 変更のような大きな変更で影響を受ける table が多いほど、Migration に要する時間は長くなります。そして table が大きいほど、Migration にかかる時間も長くなります。 +`audit_log` table を考えてみましょう。こうした table は通常、多数(数百万など)の record を持ちますが、Composite Primary Key を採用し、`user` の Primary Key に field を1つ追加するという決定だけで、schema 変更の際にそれらへも手を入れなければなりません。これらすべての table のサイズによっては、Migration の変更コストが不必要に高くなるか、場合によっては `User` の Primary Key を変更することがもはや金銭的に正当化できないほど高くなることもあります。これはたいてい、user table に unique index を追加する、といった回避策につながり、technical debt を生み、遅かれ早かれ legacy のリスト行きになります。 + +大規模プロジェクトでは、これが非常に大きなダウンタイム(数分から数時間)につながり、場合によっては、table をコピーし、ghost table に record を挿入し、Migration 後に table を行き来させるといった、まったく新しい Migration 抽象化システムの導入にまで至ることがあります。この付加的な複雑性は、Composite Primary Key を持つ別の Entity と関係を持つすべての Entity にも波及し、database 構造が大きくなるほど増大します。この問題は(Composite Primary Key を完全に取り除く以外)解決策がなく、悪化する一方です。 + +## 検索容易性 + +あなたが database administrator や Data Engineer/Scientist であれば、通常は database に直接アクセスして、必要に応じてデータを探索します。Composite Primary Key の場合、SQL を直接書くユーザーは、関与するすべての table の正しい Primary Key(および正しい Index 最適化を得るための column の順序)を知っていなければなりません。この追加の負担は、データ探索やレポート作成などを複雑にするだけでなく、Composite Primary Key が突然変更された場合に、古い SQL にエラーを引き起こすこともあります。古い SQL はおそらく依然として有効で正常に動作しますが、Composite Primary Key に新たな field が追加されたのに join にそれが欠けているせいで、突然、誤った結果を返すようになります。ここでは Primary Key を1つだけにしておくほうがはるかに簡単です。これによりデータを見つけやすくなり、たとえば user object の一意識別の方法を変更することにしたとしても、古い SQL クエリが引き続き正しく動作することを保証できます。 + +## 改訂 + +一度、ある Entity に Composite Primary Key を使ってしまうと、key の refactoring は大規模な追加 refactoring を招く可能性があります。Composite Primary Key を持つ Entity には通常、単一の一意な field が存在しないため、すべての filter と link は Composite Key のすべての値を含める必要があります。これはたいてい、code が Composite Primary Key を知っていることに依存することを意味し、そのためすべての field を取得しなければならなくなります(例: user:key1:key2 のような URL)。この key を変更すると、URL、custom SQL クエリ、その他の場所など、この知識が明示的に使われているすべての箇所を書き換えなければなりません。 + +多くの ORM は通常、値を手動指定しなくても Join を自動生成しますが、URL 構造や custom SQL クエリといった他のユースケース、特にレポーティングシステムや外部システムなど ORM をまったく使っていない場所の refactoring までを自動でカバーすることはできません。 + +## ORM の複雑さ + +Composite Primary Key をサポートすると、Deepkit ORM のような強力な ORM の code 複雑性が飛躍的に増大します。code とメンテナンスはより複雑になり、したがってより高コストになるだけでなく、ユーザーからの edge case も増え、対応と保守が必要になります。query layer、change detection、migration system、internal relationship tracking などの複雑性が大幅に増します。Composite Primary Key を備えた ORM を構築・サポートすることに伴う総コストは、あらゆる点を考慮すると高すぎて正当化できません。これが Deepkit がそれをサポートしない理由です。 + +## 利点 + +これとは別に、Composite Primary Key にも利点はありますが、非常に表層的なものです。各 table に対して可能な限り少ない Index を使うことで、書き込み(insert/update)がより効率的になります。維持すべき Index が少なくなるためです。また、model の構造も少しだけクリーンになります(通常は column が1つ少なくなるため)。しかし、逐次順序で自動増分する Primary Key と、増分しない Primary Key の差は、昨今では完全に無視できる程度です。disk space は安価であり、処理自体も通常は append-only の操作で非常に高速だからです。 + +確かに、いくつかの edge case(そして少数の非常に特定の database system)では、最初は Composite Primary Key で作業したほうが良いこともあるでしょう。しかし、そのような system であっても、(あらゆるコストを考慮すると)それらを使わず、別の戦略に切り替えるほうが全体として理にかなっている場合があります。 + +## 代替案 + +Composite Primary Key の代替は、単一の自動増分 numeric Primary Key(通常は「id」と呼ばれる)を使い、Composite Primary Key は複数 field の unique index に移すことです。使用する Primary Key(想定行数に依存)に応じて、「id」は1 record あたり 4 または 8 バイトを使用します。 + +この戦略を採用することで、前述の問題について考え、その解決策を見つけることを強制されなくなり、成長し続けるプロジェクトのコストを大幅に削減できます。 + +この戦略は具体的には、各 Entity に「id」field があり、通常は一番最初に置かれ、この field が既定で一意行の識別および Join に使われることを意味します。 + +```typescript +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor(public username: string) {} +} +``` + +Composite Primary Key の代替として、代わりに複数 field の unique index を使用します。 + +```typescript +@entity.index(['tenancyId', 'username'], {unique: true}) +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public tenancyId: number, + public username: string, + ) {} +} +``` + +Deepkit ORM は MongoDB を含め、自動増分の Primary Key を自動的にサポートします。これは database 内の record を識別するための推奨方法です。ただし MongoDB では、単純な Primary Key として ObjectId(`_id: MongoId & PrimaryKey = ''`)を使用できます。自動増分 numeric Primary Key の代替として UUID もあり、同様に機能します(ただし、indexing が高コストになるためパフォーマンス特性はやや異なります)。 + +## まとめ + +Composite Primary Key は本質的に、一度導入すると、その後のあらゆる変更と実務での利用コストが大幅に高くなることを意味します。最初は(column が1つ少ないため)クリーンなアーキテクチャに見えるものの、プロジェクトを実際に開発し始めると実務コストは顕著になり、プロジェクトが大きくなるにつれてコストは増え続けます。 + +利点と欠点の非対称性を見れば、ほとんどの場合 Composite Primary Key は正当化できないことは明らかです。コストは利点をはるかに上回ります。あなたというユーザーにとってだけでなく、ORM の code の作者およびメンテナとしての私たちにとっても同様です。このため、Deepkit ORM は Composite Primary Key をサポートしません。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/entity.md b/website/src/translations/ja/documentation/orm/entity.md new file mode 100644 index 000000000..27f669a11 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/entity.md @@ -0,0 +1,226 @@ +# エンティティ + +エンティティは Class かオブジェクトリテラル(Interface)のいずれかであり、必ず主キーを持ちます。 +エンティティには、`@deepkit/type` の型アノテーションを用いて必要な情報が付与されます。例えば、主キー、各フィールド、その検証制約などを定義します。これらのフィールドはデータベース構造(通常はテーブルまたはコレクション)を反映します。 + +`Mapped<'name'>` のような特別な型アノテーションにより、フィールド名をデータベース内の別名にマッピングすることもできます。 + +## Class + +```typescript +import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: string & Unique & MinLength<2> & MaxLength<16>, + public email: string & Unique, + ) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +await database.migrate(); + +await database.persist(new User('Peter')); + +const allUsers = await database.query(User).find(); +console.log('all users', allUsers); +``` + +## Interface + +```typescript +import { PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + username: string & Unique & MinLength<2> & MaxLength<16>; +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:')); +database.register({name: 'user'}); + +await database.migrate(); + +const user: User = {id: 0, created: new Date, username: 'Peter'}; +await database.persist(user); + +const allUsers = await database.query().find(); +console.log('all users', allUsers); +``` + +## プリミティブ + +String、Number(bigint)、Boolean のようなプリミティブデータ型は一般的なデータベース型にマッピングされます。TypeScript の型のみが使用されます。 + +```typescript + +interface User { + logins: number; + username: string; + pro: boolean; +} +``` + +## 主キー + +各エンティティには主キーがちょうど1つ必要です。複数主キーはサポートされていません。 + +主キーの基底の型は任意で、一般的には number や UUID が使われます。 +MongoDB では MongoId または ObjectID がよく使われます。 + +number の場合は `AutoIncrement` を使用できます。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## オートインクリメント + +挿入時に自動的にインクリメントされるべきフィールドには `AutoIncrement` デコレータを付与します。すべてのアダプタがオートインクリメント値をサポートします。MongoDB アダプタはカウンタを管理するために追加のコレクションを使用します。 + +オートインクリメントフィールドは自動カウンタであり、主キーにのみ適用できます。データベースは ID が一度しか使用されないことを自動的に保証します。 + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## UUID + +UUID(v4)型であるべきフィールドには UUID デコレータを付与します。実行時の型は `string` で、データベース内では多くの場合バイナリです。新しい UUID v4 を作成するには `uuid()` 関数を使用します。 + +```typescript +import { uuid, UUID, PrimaryKey } from '@deepkit/type'; + +class User { + id: UUID & PrimaryKey = uuid(); +} +``` + +## MongoDB ObjectID + +MongoDB で ObjectID 型であるべきフィールドには `MongoId` デコレータを付与します。実行時の型は `string` で、データベース内では `ObjectId`(バイナリ)です。 + +MongoID フィールドは挿入時に自動的に新しい値が割り当てられます。フィールド名として `_id` を使用する必要はありません。任意の名前にできます。 + +```typescript +import { PrimaryKey, MongoId } from '@deepkit/type'; + +class User { + id: MongoId & PrimaryKey = ''; +} +``` + +## Optional / Nullable + +Optional なフィールドは、TypeScript の型として `title?: string` または `title: string | null` のように宣言します。これらのうちどちらか一方のみを使用するべきで、通常は `undefined` とともに動作する Optional の `?` 構文を用います。 +どちらの書き方でも、すべての SQL アダプタにおいてデータベース型は `NULLABLE` になります。したがって、この2つの記法の違いは、実行時に表す値が異なる点だけです。 + +次の例では、modified フィールドは Optional であるため実行時には undefined になり得ますが、データベースでは常に NULL として表現されます。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified?: Date; +} +``` + +この例は nullable な型がどのように機能するかを示しています。データベースと JavaScript の実行時の両方で NULL が使用されます。これは `modified?: Date` よりも冗長であり、一般的には使用されません。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified: Date | null = null; +} +``` + +## データベース型マッピング + +|=== +|実行時の型|SQLite|MySQL|Postgres|Mongo + +|string|text|longtext|text|string +|number|float|double|double precision|int/number +|boolean|integer(1)|boolean|boolean|boolean +|date|text|datetime|timestamp|datetime +|array|text|json|jsonb|array +|map|text|json|jsonb|object +|map|text|json|jsonb|object +|union|text|json|jsonb|T +|uuid|blob|binary(16)|uuid|binary +|ArrayBuffer/Uint8Array/...|blob|longblob|bytea|binary +|=== + +`DatabaseField` を使用すると、フィールドを任意のデータベース型にマッピングできます。型は有効な SQL 文でなければならず、そのままマイグレーションシステムに渡されます。 + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + title: string & DatabaseField<{type: 'VARCHAR(244)'}>; +} +``` + +特定のデータベース向けにフィールドをマッピングするには、`SQLite`、`MySQL`、`Postgres` のいずれかを使用できます。 + +### SQLite + +```typescript +import { SQLite } from '@deepkit/type'; + +interface User { + title: string & SQLite<{type: 'text'}>; +} +``` + +### MySQL + +```typescript +import { MySQL } from '@deepkit/type'; + +interface User { + title: string & MySQL<{type: 'text'}>; +} +``` + +### Postgres + +```typescript +import { Postgres } from '@deepkit/type'; + +interface User { + title: string & Postgres<{type: 'text'}>; +} +``` + +## 埋め込み型 + +## デフォルト値 + +## デフォルト式 + +## 複合型 + +## 除外 + +## データベース固有のカラム型 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/events.md b/website/src/translations/ja/documentation/orm/events.md new file mode 100644 index 000000000..b1b045ebc --- /dev/null +++ b/website/src/translations/ja/documentation/orm/events.md @@ -0,0 +1,63 @@ +# イベント + +イベントは Deepkit ORM にフックするための手段であり、強力なプラグインを作成できるようにします。イベントには2つのカテゴリがあります: Query イベントと Unit-of-Work イベントです。プラグインの作者は通常、両方の方法によるデータ操作をサポートするために両方を使用します。 + +イベントはイベントトークンに対して `Database.listen` を介して登録されます。短命なイベントリスナーはセッション上にも登録できます。 + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); +database.listen(Query.onFetch, async (event) => { +}); + +const session = database.createSession(); + +//この特定のセッションに対してのみ実行されます +session.eventDispatcher.listen(Query.onFetch, async (event) => { +}); +``` + +## Query イベント + +Query イベントは、`Database.query()` または `Session.query()` を介してクエリが実行されたときに発火します。 + +各イベントには、エンティティの型、クエリ自体、データベースセッションなどの追加のプロパティがあります。`Event.query` に新しいクエリを設定することで、クエリを上書きできます。 + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); + +const unsubscribe = database.listen(Query.onFetch, async event => { + //ユーザーのクエリを上書きし、別の処理が実行されるようにします。 + event.query = event.query.filterField('fieldName', 123); +}); + +//フックを削除するには unsubscribe を呼び出します +unsubscribe(); +``` + +「Query」には複数のイベントトークンがあります: + +| イベントトークン | 説明 | +|--------------------|-------------------------------------------------------------| +| Query.onFetch | find()/findOne()/等でオブジェクトが取得されたとき | +| Query.onDeletePre | deleteMany/deleteOne() によってオブジェクトが削除される前 | +| Query.onDeletePost | deleteMany/deleteOne() によってオブジェクトが削除された後 | +| Query.onPatchPre | patchMany/patchOne() によってオブジェクトがパッチ/更新される前 | +| Query.onPatchPost | patchMany/patchOne() によってオブジェクトがパッチ/更新された後 | + +## Unit Of Work イベント + +Unit-of-Work イベントは、新しいセッションが変更を送信したときにトリガーされます。 + +| イベントトークン | 説明 | +|----------------------------------|--------------------------------------------------------------------------------------------------------------| +| DatabaseSession.onUpdatePre | `DatabaseSession` オブジェクトがデータベースレコードに対する更新操作を開始する直前にトリガーされます。 | +| DatabaseSession.onUpdatePost | `DatabaseSession` オブジェクトが更新操作を正常に完了した直後にトリガーされます。 | +| DatabaseSession.onInsertPre | `DatabaseSession` オブジェクトがデータベースへの新規レコードの挿入を開始する直前にトリガーされます。 | +| DatabaseSession.onInsertPost | `DatabaseSession` オブジェクトが新規レコードの挿入に成功した直後にトリガーされます。 | +| DatabaseSession.onDeletePre | `DatabaseSession` オブジェクトがデータベースからレコードを削除する操作を開始する直前にトリガーされます。 | +| DatabaseSession.onDeletePost | `DatabaseSession` オブジェクトが削除操作を完了した直後にトリガーされます。 | +| DatabaseSession.onCommitPre | `DatabaseSession` オブジェクトがセッション中に行われた変更をデータベースにコミットする直前にトリガーされます。 | \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/getting-started.md b/website/src/translations/ja/documentation/orm/getting-started.md new file mode 100644 index 000000000..a4790021b --- /dev/null +++ b/website/src/translations/ja/documentation/orm/getting-started.md @@ -0,0 +1,191 @@ +# はじめに + +Deepkit はデータベースにモダンな方法でアクセスできる Database ORM を提供します。 +エンティティは TypeScript の Type を使ってシンプルに定義できます: + +```typescript +import { entity, PrimaryKey, AutoIncrement, + Unique, MinLength, MaxLength } from '@deepkit/type'; + +type Username = string & Unique & MinLength<2> & MaxLength<16>; + +// クラスのエンティティ +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: Username, + public email: string & Unique, + ) {} +} + +// もしくは Interface で +interface User { + id: number & PrimaryKey & AutoIncrement; + created: Date; + firstName?: string; + lastName?: string; + username: Username; + email: string & Unique; +} +``` + +Deepkit のあらゆる TypeScript の Type と検証デコレーターを使って、エンティティを完全に定義できます。 +エンティティの型システムは、これらの Type や Class を HTTP ルート、RPC アクション、フロントエンドなどの他の領域でも使用できるように設計されています。これにより、例えばアプリケーション全体のあちこちでユーザーを何度も定義してしまうといったことを防げます。 + +## インストール + +Deepkit ORM は Runtime Types に基づいているため、`@deepkit/type` が正しくインストールされている必要があります。 +[Runtime Type のインストール](../runtime-types/getting-started.md)を参照してください。 + +これが完了したら、`@deepkit/orm` 本体とデータベースアダプターをインストールできます。 + +Class をエンティティとして使用する場合は、tsconfig.json で `experimentalDecorators` を有効にする必要があります: + +```json +{ + "compilerOptions": { + "experimentalDecorators": true + } +} +``` + +ライブラリをインストールしたら、データベースアダプターを導入し、その API を直接利用できます。 + +### SQLite + +```sh +npm install @deepkit/orm @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; + +const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +``` + +### MySQL + +```sh +npm install @deepkit/orm @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; + +const database = new Database(new MySQLDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### Postgres + +```sh +npm install @deepkit/orm @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/postgres'; + +const database = new Database(new PostgresDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### MongoDB + +```sh +npm install @deepkit/orm @deepkit/bson @deepkit/mongo +``` + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(new MongoDatabaseAdapter('mongodb://localhost/mydatabase'), [User]); +``` + +## 使い方 + +主に `Database` オブジェクトを使用します。インスタンス化すると、アプリケーション全体でデータのクエリや操作に利用できます。データベースへの接続は遅延初期化されます。 + +`Database` オブジェクトにはアダプターを渡します。これは各データベースアダプターのライブラリから提供されます。 + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + @entity.name('user') + class User { + public id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); + await database.migrate(); // テーブルを作成 + + await database.persist(new User('Peter')); + + const allUsers = await database.query(User).find(); + console.log('all users', allUsers); +} + +main(); +``` + +### データベース + +### 接続 + +#### リードレプリカ + +## リポジトリ + +## インデックス + +## 大文字・小文字の区別 + +## 文字セット + +## 照合順序 + +## バッチ処理 + +## キャッシュ + +## マルチテナンシー + +## 命名戦略 + +## ロック + +### 楽観的ロック + +### 悲観的ロック + +## カスタム Type + +## ロギング + +## マイグレーション + +## シーディング + +## 生のデータベースアクセス + +### SQL + +### MongoDB + +## プラグイン \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/inheritance.md b/website/src/translations/ja/documentation/orm/inheritance.md new file mode 100644 index 000000000..cbe6c5808 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/inheritance.md @@ -0,0 +1,104 @@ +# 継承 + +Deepkit ORM で継承を実装する方法はいくつかあります。 + +## クラス継承 + +その一つはクラス継承を用いる方法で、`extends` を使ったシンプルなクラスを利用します。 + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +class BaseModel { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + updated: Date = new Date; +} + +class User extends BaseModel { + name: string = ''; +} + +class Customer extends BaseModel { + name: string = ''; + address: string = ''; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [User, Customer] +); +``` + +`BaseModel` はエンティティとして使用されないため、データベースには登録されません。`User` と `Customer` のみがエンティティとして登録され、`BaseModel` のすべてのプロパティを含めてテーブルにマッピングされます。 + +SQL テーブルは次のようになります: + +```sql +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL +); + +CREATE TABLE customer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL, + address TEXT NOT NULL +); +``` + +## 単一テーブル継承 + +単一テーブル継承は、複数のエンティティを1つのテーブルに格納する方法です。各モデルごとに別々のテーブルを持つ代わりに、単一のテーブルを使用し、各レコードの種類を判別するために追加の列(しばしば type などと名付けられます)を利用します。これは、同じプロパティを共有するエンティティが多い場合に有用です。 + +```typescript +import { PrimaryKey, AutoIncrement, entity } from '@deepkit/type'; + +@entity.collection('persons') +abstract class Person { + id: number & PrimaryKey & AutoIncrement = 0; + firstName?: string; + lastName?: string; + abstract type: string; +} + +@entity.singleTableInheritance() +class Employee extends Person { + email?: string; + + type: 'employee' = 'employee'; +} + +@entity.singleTableInheritance() +class Freelancer extends Person { + @t budget: number = 10_000; + + type: 'freelancer' = 'freelancer'; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [Employee, Freelancer] +); +``` + +`Person` クラスはエンティティではないため、データベースには登録されません。`Employee` と `Freelancer` クラスはエンティティであり、`persons` という名前の単一のテーブルにマッピングされます。`type` 列は各レコードの種類を判定するために使用されます。 + +SQL テーブルは次のようになります: + +```sql +CREATE TABLE persons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + firstName TEXT, + lastName TEXT, + type TEXT NOT NULL, + email TEXT, + budget INTEGER +); +``` + +ご覧のとおり、budget は任意項目になっています(`Freelance` クラスでは必須であるにもかかわらず)。これは、同じテーブルに `Employee`(budget の値を持たない)を挿入できるようにするためです。これは単一テーブル継承の制約です。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/migrations.md b/website/src/translations/ja/documentation/orm/migrations.md new file mode 100644 index 000000000..f5dbca875 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/migrations.md @@ -0,0 +1,73 @@ +# マイグレーション + +マイグレーションは、データベーススキーマの変更を構造化され整理された方法で行うための手段です。ディレクトリ内の TypeScript ファイルとして保存され、コマンドラインツールで実行できます。 + +Deepkit Framework を使用する場合、Deepkit ORM のマイグレーションはデフォルトで有効になっています。 + +## コマンド + +- `migration:create` - データベースの差分に基づいて新しいマイグレーションファイルを生成します +- `migration:pending` - 保留中のマイグレーションファイルを表示します +- `migration:up` - 保留中のマイグレーションファイルを実行します。 +- `migration:down` - ダウンマイグレーションを実行し、古いマイグレーションファイルを元に戻します + +これらのコマンドは、FrameworkModule をインポートしたアプリケーション内、または `@deepkit/sql` の `deepkit-sql` コマンドラインツールから利用できます。 + +[FrameworkModule のマイグレーション統合](../framework/database.md#migration)は、あなたのデータベース(プロバイダとして定義する必要があります)を自動的に読み込みます。一方、`deepkit-sql` ではデータベースをエクスポートする TypeScript ファイルを指定する必要があります。後者は Deepkit Framework を使わずに Deepkit ORM を単体で使用する場合に便利です。 + +## マイグレーションの作成 + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + name = 'default'; + constructor() { + super(new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), [User]); + } +} +``` + +```sh +./node_modules/.bin/deepkit-sql migration:create --path database.ts --migrationDir src/migrations +``` + +新しいマイグレーションファイルが `src/migrations` に作成されます。 + +新しく作成されたマイグレーションファイルには、TypeScript アプリで定義されたエンティティと設定済みデータベースの差分に基づく up と down の Method が含まれます。必要に応じて up の Method を変更できます。down の Method は up の Method に基づいて自動生成されます。 +他の開発者も実行できるよう、このファイルをリポジトリにコミットします。 + +## 保留中のマイグレーション + +```sh +./node_modules/.bin/deepkit-sql migration:pending --path database.ts --migrationDir src/migrations +``` + +すべての保留中のマイグレーションが表示されます。まだ実行されていない新しいマイグレーションファイルがある場合、ここに一覧表示されます。 + +## マイグレーションの実行 + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations +``` + +次の保留中のマイグレーションを実行します。 + +## マイグレーションの取り消し + +```sh +./node_modules/.bin/deepkit-sql migration:down --path database.ts --migrationDir src/migrations +``` + +最後に実行されたマイグレーションを取り消します。 + +## フェイクマイグレーション + +たとえば、マイグレーション(up または down)を実行しようとして失敗したとします。問題を手動で修正しましたが、すでに実行済みになっているため、そのマイグレーションを再度実行できません。`--fake` オプションを使うと、実際には実行せずにデータベース上で実行済みとしてマークさせ、マイグレーションを偽装できます。これにより、次の保留中のマイグレーションを実行できます。 + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/orm-browser.md b/website/src/translations/ja/documentation/orm/orm-browser.md new file mode 100644 index 000000000..9df065962 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/orm-browser.md @@ -0,0 +1,60 @@ +# ORM ブラウザ + +Deepkit ORM Browser は、データベースのスキーマとデータを探索するための Web ベースのツールです。Deepkit Framework の上に構築されており、Deepkit ORM がサポートする任意のデータベースで使用できます。 + +![ORM ブラウザ](/assets/screenshots-orm-browser/content-editing.png) + +## インストール + +Deepkit ORM Browser は Deepkit Framework の一部で、デバッグモードが有効な場合に有効になります。 + +```typescript +import { App } from '@deepkit/app'; +import { Database } from '@deepkit/orm'; + +class MyController { + @http.GET('/') + index() { + return 'Hello World'; + } +} + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +new App({ + controllers: [MyController], + providers: [MainDatabase], + imports: [new FrameworkModule({debug: true})], +}).run(); +``` + +また、Deepkit ORM Browser をスタンドアロンのパッケージとしてインストールすることもできます。 + +```bash +npm install @deepkit/orm-browser +``` + +```typescript +// database.ts +import { Database } from '@deepkit/orm'; + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +export const database = new MainDatabase(); +``` + +次に、Deepkit ORM Browser サーバーを起動できます。 + +```sh +./node_modules/.bin/deepkit-orm-browser database.ts +``` + +これで Deepkit ORM Browser は http://localhost:9090 で利用できます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/plugin-soft-delete.md b/website/src/translations/ja/documentation/orm/plugin-soft-delete.md new file mode 100644 index 000000000..7c7968bd1 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/plugin-soft-delete.md @@ -0,0 +1,112 @@ +# Soft-Delete + +Soft-Delete プラグインは、実際に削除せずにデータベースのレコードを非表示のまま保持できます。レコードが削除された場合でも、実際には削除されず、削除済みとしてマークされるだけです。すべての query はこの deleted プロパティを自動でフィルタリングするため、ユーザーには実際に削除されたかのように見えます。 + +このプラグインを使用するには、SoftDelete Class をインスタンス化し、各 Entity に対して有効化する必要があります。 + +```typescript +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { SoftDelete } from '@deepkit/orm'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + // このフィールドはレコードがソフト削除されているかどうかの指標として使用されます。 + // 設定されていれば、そのレコードはソフト削除されています。 + deletedAt?: Date; + + // このフィールドは任意で、誰/何がそのレコードを削除したかを追跡するために使用できます。 + deletedBy?: string; + + constructor( + public name: string + ) { + } +} + +const softDelete = new SoftDelete(database); +softDelete.enable(User); + +// 再度無効化する場合 +softDelete.disable(User); +``` + +## 削除 + +レコードをソフト削除するには、通常の方法である query の `deleteOne` または `deleteMany` を使用するか、Session を使用して削除します。Soft-Delete プラグインが残りをバックグラウンドで自動的に処理します。 + +## 復元 + +削除済みレコードは、`SoftDeleteQuery` による lifted query を使って復元できます。`restoreOne` と `restoreMany` が用意されています。 + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreOne(); +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreMany(); +``` + +Session でも要素の復元をサポートしています。 + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).restore(user1); +await session.commit(); +``` + +## ハード削除 + +レコードをハード削除するには、SoftDeleteQuery による lifted query を使用します。これは本質的に、Soft-Delete プラグインを使用しない通常の動作を復元します。 + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// データベースからレコードを実際に削除する +await database.query(User).lift(SoftDeleteQuery).hardDeleteOne(); +await database.query(User).lift(SoftDeleteQuery).hardDeleteMany(); + +// 上記と同等 +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteOne(); +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteMany(); +``` + +## 削除済みをクエリする + +`SoftDeleteQuery` による「lifted」な query では、削除済みレコードも含めることができます。 + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// すべてを検索(ソフト削除済みと未削除の両方) +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().find(); + +// ソフト削除済みのみを検索 +await database.query(s).lift(SoftDeleteQuery).isSoftDeleted().count() +``` + +## Deleted by + +`deletedBy` は query と Session から設定できます。 + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).setDeletedBy('Peter'); +session.remove(user1); + +await session.commit(); +import { SoftDeleteQuery } from '@deepkit/orm'; + +database.query(User).lift(SoftDeleteQuery) +.deletedBy('Peter') +.deleteMany(); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/query.md b/website/src/translations/ja/documentation/orm/query.md new file mode 100644 index 000000000..af9a59037 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/query.md @@ -0,0 +1,356 @@ +# クエリ + +クエリは、データベースからデータを取得または変更する方法を記述するオブジェクトです。クエリを記述するための複数の Method と、それらを実行する終端 Method を持ちます。データベースアダプターは、データベース固有の機能をサポートするために、クエリ API をさまざまな方法で拡張できます。 + +`Database.query(T)` または `Session.query(T)` を使用してクエリを作成できます。パフォーマンスが向上するため、Session の使用を推奨します。 + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + birthdate?: Date; + visits: number = 0; + + constructor(public username: string) { + } +} + +const database = new Database(...); + +//[ { username: 'User1' }, { username: 'User2' }, { username: 'User2' } ] +const users = await database.query(User).select('username').find(); +``` + +## フィルター + +フィルターを適用して結果セットを絞り込むことができます。 + +```typescript +//シンプルなフィルター +const users = await database.query(User).filter({name: 'User1'}).find(); + +//複数フィルター。すべて AND +const users = await database.query(User).filter({name: 'User1', id: 2}).find(); + +//範囲フィルター: $gt, $lt, $gte, $lte(より大きい、より小さい、...) +//WHERE created < NOW() と同等 +const users = await database.query(User).filter({created: {$lt: new Date}}).find(); +//WHERE id > 500 と同等 +const users = await database.query(User).filter({id: {$gt: 500}}).find(); +//WHERE id >= 500 と同等 +const users = await database.query(User).filter({id: {$gte: 500}}).find(); + +//集合フィルター: $in, $nin(IN, NOT IN) +//WHERE id IN (1, 2, 3) と同等 +const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find(); + +//正規表現フィルター +const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find(); + +//グルーピング: $and, $nor, $or +//WHERE (username = 'User1') OR (username = 'User2') と同等 +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2'}] +}).find(); + + +//ネストされたグルーピング +//WHERE username = 'User1' OR (username = 'User2' and id > 0) と同等 +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2', id: {$gt: 0}}] +}).find(); + + +//ネストされたグルーピング +//WHERE username = 'User1' AND (created < NOW() OR id > 0) と同等 +const users = await database.query(User).filter({ + $and: [{username: 'User1'}, {$or: [{created: {$lt: new Date}, id: {$gt: 0}}]}] +}).find(); +``` + +### 等価 + +### 大なり/小なり + +### 正規表現 + +### グルーピング AND/OR + +### IN + +## Select + +データベースから受け取るフィールドを絞り込むには、`select('field1')` を使用します。 + +```typescript +const user = await database.query(User).select('username').findOne(); +const user = await database.query(User).select('id', 'username').findOne(); +``` + +`select` を使ってフィールドを絞り込んだ時点で、結果はエンティティのインスタンスではなく、オブジェクトリテラルのみになることに注意してください。 + +``` +const user = await database.query(User).select('username').findOne(); +user instanceof User; //false +``` + +## 並び順 + +`orderBy(field, order)` でエントリの並び順を変更できます。 +`orderBy` は複数回実行でき、順序を段階的に詳細化できます。 + +```typescript +const users = await session.query(User).orderBy('created', 'desc').find(); +const users = await session.query(User).orderBy('created', 'asc').find(); +``` + +## ページネーション + +`itemsPerPage()` と `page()` を使って、結果をページングできます。ページは 1 から開始します。 + +```typescript +const users = await session.query(User).itemsPerPage(50).page(1).find(); +``` + +代替の `limit` と `skip` を使えば手動でページングできます。 + +```typescript +const users = await session.query(User).limit(5).skip(10).find(); +``` + +[#database-join] +## 結合 + +デフォルトでは、エンティティからの参照はクエリに含まれず、ロードもされません。参照をロードせずにクエリに結合を含めるには、`join()`(LEFT JOIN)または `innerJoin()` を使用します。参照をロードしつつクエリに結合を含めるには、`joinWith()` または `innerJoinWith()` を使用します。 + +以下の例はすべて、次のモデルスキーマを前提としています。 + +```typescript +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + group?: Group & Reference; + + constructor(public username: string) { + } +} +``` + +```typescript +//グループが割り当てられたユーザーのみを選択(INNER JOIN) +const users = await session.query(User).innerJoin('group').find(); +for (const user of users) { + user.group; //参照がロードされていないため error +} +``` + +```typescript +//グループが割り当てられたユーザーのみを選択(INNER JOIN)し、リレーションをロード +const users = await session.query(User).innerJoinWith('group').find(); +for (const user of users) { + user.group.name; //動作します +} +``` + +結合クエリを修正するには、同じ Method を `use` プレフィックス付きで使用します: `useJoin`, `useInnerJoin`, `useJoinWith`, `useInnerJoinWith`。結合クエリの修正を終了し、親クエリに戻るには `end()` を使用します。 + +```typescript +//名前が 'admins' のグループが割り当てられているユーザーのみを選択(INNER JOIN) +const users = await session.query(User) + .useInnerJoinWith('group') + .filter({name: 'admins'}) + .end() // 親クエリに戻る + .find(); + +for (const user of users) { + user.group.name; //常に admin +} +``` + +## 集計 + +集計 Method により、レコードのカウントやフィールドの集計が可能です。 + +次の例は、このモデルスキーマを前提としています。 + +```typescript +@entity.name('file') +class File { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + downloads: number = 0; + + category: string = 'none'; + + constructor(public path: string & Index) { + } +} +``` + +`groupBy` は指定したフィールドで結果をグルーピングします。 + +```typescript +await database.persist( + cast({path: 'file1', category: 'images'}), + cast({path: 'file2', category: 'images'}), + cast({path: 'file3', category: 'pdfs'}) +); + +//[ { category: 'images' }, { category: 'pdfs' } ] +await session.query(File).groupBy('category').find(); +``` + +利用できる集計 Method は複数あります: `withSum`, `withAverage`, `withCount`, `withMin`, `withMax`, `withGroupConcat`。いずれも第1引数にフィールド名を取り、別名を変更するための任意の第2引数を取ります。 + +```typescript +// まず、いくつかのレコードを更新します: +await database.query(File).filter({path: 'images/file1'}).patchOne({$inc: {downloads: 15}}); +await database.query(File).filter({path: 'images/file2'}).patchOne({$inc: {downloads: 5}}); + +//[{ category: 'images', downloads: 20 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withSum('downloads').find(); + +//[{ category: 'images', downloads: 10 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withAverage('downloads').find(); + +//[ { category: 'images', amount: 2 }, { category: 'pdfs', amount: 1 } ] +await session.query(File).groupBy('category').withCount('id', 'amount').find(); +``` + +## Returning + +`patch` および `delete` による変更時に、`returning` で追加のフィールドを要求できます。 + +注意: すべてのデータベースアダプターがフィールドをアトミックに返すわけではありません。データ整合性を確保するためにトランザクションを使用してください。 + +```typescript +await database.query(User).patchMany({visits: 0}); + +//{ modified: 1, returning: { visits: [ 5 ] }, primaryKeys: [ 1 ] } +const result = await database.query(User) + .filter({username: 'User1'}) + .returning('username', 'visits') + .patchOne({$inc: {visits: 5}}); +``` + +## Find + +指定したフィルターに一致するエントリの配列を返します。 + +```typescript +const users: User[] = await database.query(User).filter({username: 'Peter'}).find(); +``` + +## FindOne + +指定したフィルターに一致するアイテムを返します。 +アイテムが見つからない場合、`ItemNotFound` Error がスローされます。 + +```typescript +const users: User = await database.query(User).filter({username: 'Peter'}).findOne(); +``` + +## FindOneOrUndefined + +指定したフィルターに一致するエントリを返します。 +エントリが見つからない場合は、undefined が返されます。 + +```typescript +const query = database.query(User).filter({username: 'Peter'}); +const users: User|undefined = await query.findOneOrUndefined(); +``` + +## FindField + +指定したフィルターに一致するフィールドのリストを返します。 + +```typescript +const usernames: string[] = await database.query(User).findField('username'); +``` + +## FindOneField + +指定したフィルターに一致するフィールドのリストを返します。 +エントリが見つからない場合、`ItemNotFound` Error がスローされます。 + +```typescript +const username: string = await database.query(User).filter({id: 3}).findOneField('username'); +``` + +## Patch + +Patch は、クエリで記述されたレコードをパッチする変更クエリです。`patchOne` と `patchMany` の Method がクエリを終了し、パッチを実行します。 + +`patchMany` は、指定したフィルターに一致するすべてのレコードをデータベースで変更します。フィルターが設定されていない場合は、テーブル全体が変更されます。1件のみ変更するには `patchOne` を使用します。 + +```typescript +await database.query(User).filter({username: 'Peter'}).patch({username: 'Peter2'}); + +await database.query(User).filter({username: 'User1'}).patchOne({birthdate: new Date}); +await database.query(User).filter({username: 'User1'}).patchOne({$inc: {visits: 1}}); + +await database.query(User).patchMany({visits: 0}); +``` + +## Delete + +`deleteMany` は、指定したフィルターに一致するすべてのエントリをデータベースから削除します。 +フィルターが設定されていない場合は、テーブル全体が削除されます。1件のみ削除するには `deleteOne` を使用します。 + +```typescript +const result = await database.query(User) + .filter({visits: 0}) + .deleteMany(); + +const result = await database.query(User).filter({id: 4}).deleteOne(); +``` + +## Has + +データベースに少なくとも1件のエントリが存在するかどうかを返します。 + +```typescript +const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has(); +``` + +## Count + +エントリ数を返します。 + +```typescript +const userCount: number = await database.query(User).count(); +``` + +## Lift + +クエリのリフトとは、クエリに新しい機能を追加することを意味します。これは通常、プラグインや複雑なアーキテクチャで、大きなクエリ Class を複数の使いやすく再利用可能な Class に分割するために使用されます。 + +```typescript +import { FilterQuery, Query } from '@deepkit/orm'; + +class UserQuery extends Query { + hasBirthday() { + const start = new Date(); + start.setHours(0,0,0,0); + const end = new Date(); + end.setHours(23,59,59,999); + + return this.filter({$and: [{birthdate: {$gte: start}}, {birthdate: {$lte: end}}]} as FilterQuery); + } +} + +await session.query(User).lift(UserQuery).hasBirthday().find(); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/raw-access.md b/website/src/translations/ja/documentation/orm/raw-access.md new file mode 100644 index 000000000..e01019cf5 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/raw-access.md @@ -0,0 +1,77 @@ +# Raw アクセス + +ORM でサポートされていない SQL クエリを実行するなど、データベースに直接アクセスする必要があることはよくあります。これは `Database` Class の `raw` Method を使用して行えます。 + +```typescript +import { PrimaryKey, AutoIncrement, @entity } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; +import { sql } from '@deepkit/sql'; +import { SqliteDatabaseAdapter } from '@deepkit/sqlite'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + constructor(public username: string) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + +const query = 'Pet%'; +const rows = await database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`).find(); + +const result = await database.raw<{ count: number }>(sql`SELECT count(*) as count FROM users WHERE username LIKE ${query}`).findOne(); +console.log('Found', result.count, 'users'); +``` + +SQL クエリは `sql` template string tag を使って構築します。これは値を Parameters として渡せる特別な template string tag です。これらの Parameters は自動的に解析され、安全な prepared statement に変換されます。これは SQLインジェクション攻撃 を避けるために重要です。 + +カラム名のような動的な identifier を渡すには、`identifier` を使用します: + +```typescript +import { identifier, sql } from '@deepkit/sql'; + +let column = 'username'; +const rows = await database.raw(sql`SELECT * FROM users WHERE ${identifier(column)} LIKE ${query}`).find(); +``` + +SQL adapter では、`raw` Method は結果を取得するための `findOne` と `find` Method を備えた `RawQuery` を返します。UPDATE/DELETE など、行を返さない SQL を実行するには、`execute` を使用できます: + +```typescript +let username = 'Peter'; +await database.raw(sql`UPDATE users SET username = ${username} WHERE id = 1`).execute(); +``` + +また、`RawQuery` は database adapter 用に正しくフォーマットされた最終的な SQL string と parameters を取得することもサポートしています: + +```typescript +const query = database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`); +console.log(query.sql); +console.log(query.params); +``` + +このようにして、その SQL を別の database client などで実行するために利用できます。 + +## Types + +`raw` には任意の Type を渡すことができ、database からの結果は自動的にその Type に変換されます。これは特に SQL adapter で有用で、Class を渡すと、その Class に自動的に変換されます。 + +ただし制限があります。この方法では SQL の JOIN はサポートされません。JOIN を使いたい場合は、ORM の query builder を使用する必要があります。 + +## Mongo + +MongoDB adapter は SQL クエリではなく Mongo の Command に基づいているため、少し動作が異なります。 + +Command には、aggregation pipeline、find クエリ、write Command などがあります。 + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase')); + +// 最初の引数はエントリポイントの collection、2番目は Command の Return Type です +const items = await database.raw([ + { $match: { roomId: 'room1' } }, + { $group: { _id: '$userId', count: { $sum: 1 } } }, +]).find(); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/relations.md b/website/src/translations/ja/documentation/orm/relations.md new file mode 100644 index 000000000..eba66d083 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/relations.md @@ -0,0 +1,153 @@ +# リレーション + +リレーションは、2つのエンティティを特定の方法で関連付けることができます。これは通常、データベースでは外部キーの概念を用いて行われます。Deepkit ORM は公式のすべてのデータベースアダプタでリレーションをサポートします。 + +リレーションは `Reference` デコレータで注釈付けします。通常、リレーションには逆方向のリレーションもあり、これは `BackReference` 型で注釈付けしますが、データベースクエリで逆方向のリレーションを使用する場合にのみ必要です。バックリファレンスは仮想的なものに過ぎません。 + +## 1対多 + +参照を保持するエンティティは、通常 `owning side`、または参照を `owns` している側と呼ばれます。次のコードは `User` と `Post` の間の 1対多のリレーションを示します。これは、1人の `User` が複数の `Post` を持てることを意味します。`post` エンティティは `post->user` リレーションを持ちます。データベースには `Post."author"` フィールドが作成され、そこに `User` の主キーが格納されます。 + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement, + Reference } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.collection('posts') +class Post { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor( + public author: User & Reference, + public title: string + ) { + } +} + +const database = new Database( + new SQLiteDatabaseAdapter(':memory:'), + [User, Post] +); +await database.migrate(); + +const user1 = new User('User1'); +const post1 = new Post(user1, 'My first blog post'); +const post2 = new Post(user1, 'My second blog post'); + +await database.persist(user1, post1, post2); +``` + +参照は、デフォルトではクエリで選択されません。詳細は [データベースの結合](./query.md#join) を参照してください。 + +## 多対1 + +参照には通常、多対1と呼ばれる逆参照があります。これはデータベース自体には反映されないため、仮想的な参照に過ぎません。バックリファレンスは `BackReference` で注釈付けされ、主にリフレクションとクエリの結合に使用されます。`User` から `Post` への `BackReference` を追加すると、`User` のクエリから直接 `Post` を結合できます。 + +```typescript +@entity.name('user').collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + posts?: Post[] & BackReference; + + constructor(public username: string) { + } +} +``` + +```typescript +//[ { username: 'User1', posts: [ [Post], [Post] ] } ] +const users = await database.query(User) + .select('username', 'posts') + .joinWith('posts') + .find(); +``` + +## 多対多 + +多対多リレーションでは、多くのレコードを他の多くのレコードに関連付けることができます。たとえば、ユーザーとグループに使用できます。ユーザーは 0 個、1 個、または複数のグループに所属できます。結果として、グループも 0 人、1 人、または複数のユーザーを含むことができます。 + +多対多のリレーションは通常、ピボットエンティティを使用して実装します。ピボットエンティティは 2 つの他のエンティティへの実際の所有側の参照を保持し、これら 2 つのエンティティはピボットエンティティへのバックリファレンスを持ちます。 + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + groups?: Group[] & BackReference<{via: typeof UserGroup}>; + + constructor(public username: string) { + } +} + +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + + users?: User[] & BackReference<{via: typeof UserGroup}>; + + constructor(public name: string) { + } +} + +// ピボットエンティティ +@entity.name('userGroup') +class UserGroup { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public user: User & Reference, + public group: Group & Reference, + ) { + } +} +``` + +これらのエンティティを使って、ユーザーとグループを作成し、ピボットエンティティでそれらを関連付けられます。User にバックリファレンスを使用することで、User のクエリでグループを直接取得できます。 + +```typescript +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Group, UserGroup]); +await database.migrate(); + +const user1 = new User('User1'); +const user2 = new User('User2'); +const group1 = new Group('Group1'); + +await database.persist(user1, user2, group1, new UserGroup(user1, group1), new UserGroup(user2, group1)); + +//[ +// { id: 1, username: 'User1', groups: [ [Group] ] }, +// { id: 2, username: 'User2', groups: [ [Group] ] } +// ] +const users = await database.query(User) + .select('username', 'groups') + .joinWith('groups') + .find(); +``` + +ユーザーとグループの関連付けを解除するには、UserGroup レコードを削除します: + +```typescript +const users = await database.query(UserGroup) + .filter({user: user1, group: group1}) + .deleteOne(); +``` + +## 1対1 + +## 制約 + +削除/更新時: RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/seeding.md b/website/src/translations/ja/documentation/orm/seeding.md new file mode 100644 index 000000000..b609502ff --- /dev/null +++ b/website/src/translations/ja/documentation/orm/seeding.md @@ -0,0 +1,3 @@ +# シーディング + +Database seeding は、データベースにデータを初期投入することです。これはデータベースの初期データ投入の一形態であり、開発・テスト、または本番データベースの初期化時に使用することを意図しています。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/session.md b/website/src/translations/ja/documentation/orm/session.md new file mode 100644 index 000000000..2eb70e4f9 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/session.md @@ -0,0 +1,59 @@ +# セッション / ユニット・オブ・ワーク + +セッションはユニット・オブ・ワークのようなものです。あなたが行うすべてを追跡し、`commit()` が呼び出されるたびに変更を自動的に記録します。ステートメントを束ねて非常に高速にするため、データベースの変更を実行する推奨の方法です。セッションは非常に軽量で、たとえばリクエスト/レスポンスのライフサイクル内で簡単に作成できます。 + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + + @entity.name('user') + class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + await database.migrate(); + + const session = database.createSession(); + session.add(new User('User1'), new User('User2'), new User('User3')); + + await session.commit(); + + const users = await session.query(User).find(); + console.log(users); +} + +main(); +``` + +`session.add(T)` で新しいインスタンスをセッションに追加し、`session.remove(T)` で既存のインスタンスを削除します。Session オブジェクトの使用が終わったら、ガーベジコレクタが回収できるように、すべての場所で単に参照を解除してください。 + +Session オブジェクト経由で取得したエンティティインスタンスの変更は自動的に検出されます。 + +```typescript +const users = await session.query(User).find(); +for (const user of users) { + user.name += ' changed'; +} + +await session.commit();//すべてのユーザーを保存 +``` + +## アイデンティティマップ + +セッションはアイデンティティマップを提供し、各データベースエントリにつき常に 1 つの JavaScript オブジェクトだけが存在することを保証します。例えば、同一のセッション内で `session.query(User).find()` を 2 回実行すると、2 つの異なる配列が得られますが、その中に入っているエンティティインスタンスは同一です。 + +`session.add(entity1)` で新しいエンティティを追加し、それを再取得すると、まったく同じエンティティインスタンス `entity1` が返されます。 + +重要: セッションを使い始めたら、`database.query` の代わりに `session.query` メソッドを使用すべきです。アイデンティティマッピング機能が有効なのはセッションのクエリだけです。 + +## 変更検出 + +## リクエスト/レスポンス \ No newline at end of file diff --git a/website/src/translations/ja/documentation/orm/transactions.md b/website/src/translations/ja/documentation/orm/transactions.md new file mode 100644 index 000000000..1aa074dc5 --- /dev/null +++ b/website/src/translations/ja/documentation/orm/transactions.md @@ -0,0 +1,85 @@ +# トランザクション + +トランザクションは、select、insert、update、delete といったステートメント、クエリ、または操作の連続したグループで、1 つの作業単位として実行され、コミットまたはロールバックが可能です。 + +Deepkit は、公式にサポートされているすべてのデータベースでトランザクションをサポートします。既定では、いかなるクエリやデータベースセッションにもトランザクションは使用されません。トランザクションを有効にするには、主に 2 つの方法(セッションとコールバック)があります。 + +## セッション・トランザクション + +作成する各セッションに対して新しいトランザクションを開始して割り当てることができます。これはデータベースとやり取りする推奨の方法で、Session オブジェクトを簡単に渡すことができ、このセッションによって生成されたすべてのクエリは自動的にそのトランザクションに割り当てられます。 + +一般的なパターンとしては、すべての操作を try-catch ブロックで囲み、最後の行で `commit()` を実行します(これはそれ以前のすべてのコマンドが成功した場合にのみ実行されます)。エラーが発生したら catch ブロックで `rollback()` を実行し、すべての変更をすぐに巻き戻します。 + +代替の API(下記参照)はありますが、すべてのトランザクションはデータベースセッションオブジェクトでのみ機能します。データベースセッションのユニット・オブ・ワークの未反映変更をデータベースにコミットするには、通常 `commit()` を呼び出します。トランザクション・セッションでは、`commit()` は保留中の変更をデータベースにコミットするだけでなく、トランザクション自体も完了(「コミット」)して閉じます。代わりに `session.flush()` を呼び出して、`commit` せずに(つまりトランザクションを閉じずに)保留中の変更をすべてコミットすることもできます。ユニット・オブ・ワークをフラッシュせずにトランザクションのみをコミットするには、`session.commitTransaction()` を使用します。 + +```typescript +const session = database.createSession(); + +//これにより新しいトランザクションが割り当てられ、次のデータベース操作のタイミングで開始されます。 +session.useTransaction(); + +try { + //このクエリはトランザクション内で実行されます + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); + + await session.commit(); +} catch (error) { + await session.rollback(); +} +``` + +セッションで `commit()` または `rollback()` が実行されると、トランザクションは解放されます。新しいトランザクションで続行したい場合は、再度 `useTransaction()` を呼び出す必要があります。 + +注意点として、トランザクション・セッションで最初のデータベース操作が実行されると、割り当てられたデータベース接続は現在のセッションオブジェクトに固定され、専有(スティッキー)されます。そのため、その後のすべての操作は同じ接続(つまり多くのデータベースでは同じデータベースサーバー)上で実行されます。トランザクション・セッションが終了(commit または rollback)したときにのみ、データベース接続は再び解放されます。したがって、トランザクションは必要最小限の期間にとどめることを推奨します。 + +セッションがすでにトランザクションに接続されている場合、`session.useTransaction()` の呼び出しは常に同じオブジェクトを返します。セッションにトランザクションが関連付けられているかどうかは、`session.isTransaction()` で確認できます。 + +入れ子のトランザクションはサポートされません。 + +## トランザクション・コールバック + +トランザクション・セッションの代替として、`database.transaction(callback)` があります。 + +```typescript +await database.transaction(async (session) => { + //このクエリはトランザクション内で実行されます + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); +}); +``` + +`database.transaction(callback)` メソッドは、新しいトランザクション・セッション内で非同期コールバックを実行します。コールバックが成功した場合(つまり、エラーがスローされない場合)、セッションは自動的にコミットされます(その結果、トランザクションがコミットされ、すべての変更がフラッシュされます)。コールバックが失敗した場合、セッションは自動的に `rollback()` を実行し、エラーは伝播されます。 + +## 分離レベル + +多くのデータベースは異なる種類のトランザクションをサポートしています。トランザクションの動作を変更するには、`useTransaction()` から返されるトランザクションオブジェクトに対してさまざまなメソッドを呼び出します。このトランザクションオブジェクトのインターフェイスは、使用しているデータベースアダプタによって異なります。たとえば、MySQL データベースから返されるトランザクションオブジェクトは、MongoDB データベースのものとは異なるオプションを持ちます。可能なオプションの一覧を取得するには、コード補完を利用するか、データベースアダプタのインターフェイスを参照してください。 + +```typescript +const database = new Database(new MySQLDatabaseAdapter()); + +const session = database.createSession(); +session.useTransaction().readUncommitted(); + +try { + //...操作 + await session.commit(); +} catch (error) { + await session.rollback(); +} + +//または +await database.transaction(async (session) => { + //これは、いかなるデータベース操作も実行されていない限り有効です。 + session.useTransaction().readUncommitted(); + + //...操作 +}); +``` + +MySQL、PostgreSQL、SQLite ではデフォルトでトランザクションが機能しますが、MongoDB ではまず「レプリカセット」としてセットアップする必要があります。 + +標準の MongoDB インスタンスをレプリカセットに変換するには、公式ドキュメントを参照してください: +[スタンドアロンをレプリカセットに変換する](https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set)。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/angular-ssr.md b/website/src/translations/ja/documentation/package/angular-ssr.md new file mode 100644 index 000000000..458d92b72 --- /dev/null +++ b/website/src/translations/ja/documentation/package/angular-ssr.md @@ -0,0 +1,128 @@ +# API `@deepkit/angular-ssr` + +```shell +npm install @deepkit/angular-ssr +``` + + +- メインのアプリケーションを `app.ts` に配置し、`angular.json` で設定してください: + +`src/server/app.ts` 内: + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { AngularModule, RequestHandler } from '@deepkit/angular-ssr'; + +const app = new App({ + controllers: [ + // あなたの controllers + ], + providers: [ + // あなたの providers + ], + imports: [ + new FrameworkModule({}), + new AngularModule({ + moduleUrl: import.meta.url, + }) + ] +}); + +const main = isMainModule(import.meta.url); + +if (main) { + void app.run(); // server:start を含むすべての CLI コマンドを呼び出せるようにする +} + +export const reqHandler = main + // main のときは、新しい request handler を作成したくない + ? () => undefined + : app.get(RequestHandler).create(); +``` + +```json +{ + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/app", + "index": "src/index.html", + "server": "src/main.server.ts", + "outputMode": "server", + "ssr": { + "entry": "src/server/app.ts" + }, + "browser": "src/main.ts" + } +} +``` + +`tsconfig` にも `src/server/app.ts` を含めていることを確認してください。 + +## Angular アプリの設定 + +`app/app.config.ts`(クライアント側): + +```typescript +@Injectable() +export class APIInterceptor implements HttpInterceptor { + constructor(@Inject('baseUrl') @Optional() private baseUrl: string) { + // クライアントビルドでは、`baseUrl` は空であり、現在の location から推論されるべきです。 + // これが正しくない場合は、`appConfig` オブジェクトの `providers` 配列で `baseUrl` を定義するだけで構いません。 + this.baseUrl = baseUrl || (typeof location !== 'undefined' ? location.origin : ''); + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const apiReq = req.clone({ url: `${this.baseUrl}/${req.url}` }); + return next.handle(apiReq); + } +} + +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: APIInterceptor, + multi: true, + }, + // 他の providers + ], +}; +``` + +`app/app.server.config.ts`(サーバー側): + +```typescript +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering(), + provideServerRouting([ + { + path: '**', + renderMode: RenderMode.Server, + }, + ]), + { + provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP, + deps: [REQUEST_CONTEXT], + useFactory(context: any) { + return { [context?.baseUrl]: context?.publicBaseUrl || '' }; + }, + }, + { + provide: 'baseUrl', + deps: [REQUEST_CONTEXT], + useFactory: (context: any) => { + return context?.baseUrl || ''; + }, + } + ], +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); +``` + + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/api-console.md b/website/src/translations/ja/documentation/package/api-console.md new file mode 100644 index 000000000..c9de38247 --- /dev/null +++ b/website/src/translations/ja/documentation/package/api-console.md @@ -0,0 +1,40 @@ +# Deepkit API コンソール + +```bash +npms install @deepkit/api-console-module +``` + +HTTP と RPC API の自動ドキュメント化。TypeScript の type 構文で、すべての routes、actions、parameters、return types、status codes を表示します。 + +これは [フレームワークデバッガー](../framework.md) の一部ですが、単体でも使用できます。 + +```typescript +import { ApiConsoleModule } from '@deepkit/api-console-module'; + +new App({ + imports: [ + new ApiConsoleModule({ + path: '/api', + markdown: ` + # 私の API + + これは私の API ドキュメントです。 + + お楽しみください! + ` + }), + ] +}) +``` + +デフォルトでは `new ApiConsoleModule` はすべての HTTP および RPC routes を表示します。`ApiConsoleModule` Class の methods を使って、表示する routes を指定することもできます。 + + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/app.md b/website/src/translations/ja/documentation/package/app.md new file mode 100644 index 000000000..52a46f1a7 --- /dev/null +++ b/website/src/translations/ja/documentation/package/app.md @@ -0,0 +1,14 @@ +# API `@deepkit/app` + +```shell +npm install @deepkit/app +``` + +コマンドラインインターフェイス (CLI) パーサー、service container と dependency injection、event dispatcher、app module system、configuration loader を提供します。 + +これは Deepkit アプリケーションを作成するためのコアです。 +CLI controller、HTTP controller や route、RPC controller([framework module](../framework.md) 経由)、およびその他の services を登録できます。 + +詳しくは [Deepkit App ドキュメント](../app.md) を参照してください。 + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/bench.md b/website/src/translations/ja/documentation/package/bench.md new file mode 100644 index 000000000..dd712f8e1 --- /dev/null +++ b/website/src/translations/ja/documentation/package/bench.md @@ -0,0 +1,35 @@ +# API `@deepkit/bench` + +```sh +npm install @deepkit/bench +``` + +コードスニペットをベンチマークするためのシンプルなツール。 + +```typescript +import { benchmark, run } from '@deepkit/bench'; + +// ASCII バイナリパースの例 +const binaryString = Buffer.from('Hello World', 'utf8'); +const codes = [ + +benchmark('Buffer.toString', () => { + const utf8String = binaryString.toString('utf8'); +}); + +benchmark('String.fromCodePoint', () => { + const utf8String = String.fromCodePoint() +}); + +void run(); +``` + +```sh +$ node --import @deepkit/run benchmarks/ascii-parsing.ts +Node v22.13.1 + 🏎 x 20,326,482.53 ops/sec ± 4.95% 0.000049 ms/op ▆▆▇▅▆▆▅▅▆▅▅▅▅▅▅▅▅▅▅▅▅▅ Buffer.toString 19850001 samples + 🏎 x 36,012,545.69 ops/sec ± 1.78% 0.000028 ms/op ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ String.fromCodePoint 35800001 samples +done +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/broker-redis.md b/website/src/translations/ja/documentation/package/broker-redis.md new file mode 100644 index 000000000..17cb8fab8 --- /dev/null +++ b/website/src/translations/ja/documentation/package/broker-redis.md @@ -0,0 +1,31 @@ +# API `@deepkit/broker-redis` + +```sh +npm install @deepkit/broker-redis +``` + +Deepkit Broker の Redis ベースの実装を提供します。内部では ioredis を使用しています。 + +この adapter は Deepkit Broker の queue adapter を実装していません。 + +```typescript +import { BrokerKeyValue, BrokerBus } from '@deepkit/broker'; +import { BrokerRedisAdapter } from '@deepkit/broker-redis'; +import { ConsoleLogger } from '@deepkit/logger'; + +const adapter = new RedisBrokerAdapter({ + preifx: 'myapp:', + host: 'localhost', + port: 6379, + // 任意。Redis サーバーが認証を必要とする場合 + // password: 'your-password', // Optional, if your Redis server requires authentication + // 任意。別の Redis データベースを指定する場合 + // db: 0, // Optional, to specify a different Redis database +}, new ConsoleLogger()); + +const keyValye = new BrokerKeyValue(adapter); +const bus = new BrokerBus(adapter); +// ... +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/broker.md b/website/src/translations/ja/documentation/package/broker.md new file mode 100644 index 000000000..dcfb9b16c --- /dev/null +++ b/website/src/translations/ja/documentation/package/broker.md @@ -0,0 +1,9 @@ +# API `@deepkit/broker` + +```sh +npm install @deepkit/broker +``` + +このパッケージには Deepkit Broker の抽象化に加えて、サーバー実装、およびアダプターとしての複数のクライアント実装(`BrokerDeepkitAdapter`、`BrokerMemoryAdapter`)が含まれます。 + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/bson.md b/website/src/translations/ja/documentation/package/bson.md new file mode 100644 index 000000000..0c74fffe8 --- /dev/null +++ b/website/src/translations/ja/documentation/package/bson.md @@ -0,0 +1,9 @@ +# API `@deepkit/bson` + +```shell +npm install @deepkit/bson +``` + +BSON と JavaScript objects を相互に変換するための encoder/decoder functions を提供します。 + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/bun.md b/website/src/translations/ja/documentation/package/bun.md new file mode 100644 index 000000000..ebc847bad --- /dev/null +++ b/website/src/translations/ja/documentation/package/bun.md @@ -0,0 +1,7 @@ +# API `@deepkit/bun` + +```shell +npm install @deepkit/bun +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/core-rxjs.md b/website/src/translations/ja/documentation/package/core-rxjs.md new file mode 100644 index 000000000..bca3c9544 --- /dev/null +++ b/website/src/translations/ja/documentation/package/core-rxjs.md @@ -0,0 +1,7 @@ +# API `@deepkit/core-rxjs` + +```shell +npm install @deepkit/core-rxjs +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/core.md b/website/src/translations/ja/documentation/package/core.md new file mode 100644 index 000000000..b0e2ccdf0 --- /dev/null +++ b/website/src/translations/ja/documentation/package/core.md @@ -0,0 +1,7 @@ +# API `@deepkit/core` + +```shell +npm install @deepkit/core +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/devtool.md b/website/src/translations/ja/documentation/package/devtool.md new file mode 100644 index 000000000..99758d66e --- /dev/null +++ b/website/src/translations/ja/documentation/package/devtool.md @@ -0,0 +1,5 @@ +# Devtool + +Deepkit Devtool は、Chrome DevTools で RPC 接続をデバッグできるようにする Chrome プラグインです。 + +[Deepkit Devtool](https://chromewebstore.google.com/detail/deepkit-devtool/lkncgbbafldohehlfdnkflbeapckdnlj) \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/event.md b/website/src/translations/ja/documentation/package/event.md new file mode 100644 index 000000000..a0116e5fa --- /dev/null +++ b/website/src/translations/ja/documentation/package/event.md @@ -0,0 +1,9 @@ +# API `@deepkit/event` + +```shell +npm install @deepkit/event +``` + +アプリケーションでイベントを使用する方法の詳細については、[アプリのイベント](../app/events.md)を参照してください。 + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/filesystem-aws-s3.md b/website/src/translations/ja/documentation/package/filesystem-aws-s3.md new file mode 100644 index 000000000..942173a3b --- /dev/null +++ b/website/src/translations/ja/documentation/package/filesystem-aws-s3.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-aws-s3` + +```shell +npm install @deepkit/filesystem-aws-s3 +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/filesystem-database.md b/website/src/translations/ja/documentation/package/filesystem-database.md new file mode 100644 index 000000000..9b79fd05e --- /dev/null +++ b/website/src/translations/ja/documentation/package/filesystem-database.md @@ -0,0 +1,9 @@ +# API `@deepkit/filesystem-database` + +```shell +npm install @deepkit/filesystem-database +``` + +サポートされているすべての Deepkit ORM データベースをファイルシステムのバックエンドとして使用できるようにします。 + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/filesystem-ftp.md b/website/src/translations/ja/documentation/package/filesystem-ftp.md new file mode 100644 index 000000000..d01521c0a --- /dev/null +++ b/website/src/translations/ja/documentation/package/filesystem-ftp.md @@ -0,0 +1,8 @@ +# API `@deepkit/filesystem-ftp` + +```shell +npm install @deepkit/filesystem-ftp +``` + + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/filesystem-google.md b/website/src/translations/ja/documentation/package/filesystem-google.md new file mode 100644 index 000000000..7345966bd --- /dev/null +++ b/website/src/translations/ja/documentation/package/filesystem-google.md @@ -0,0 +1,8 @@ +# API `@deepkit/filesystem-google` + +```shell +npm install @deepkit/filesystem-google +``` + + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/filesystem-sftp.md b/website/src/translations/ja/documentation/package/filesystem-sftp.md new file mode 100644 index 000000000..719ebfd37 --- /dev/null +++ b/website/src/translations/ja/documentation/package/filesystem-sftp.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-sftp` + +```shell +npm install @deepkit/filesystem-sftp +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/filesystem.md b/website/src/translations/ja/documentation/package/filesystem.md new file mode 100644 index 000000000..68cc87b21 --- /dev/null +++ b/website/src/translations/ja/documentation/package/filesystem.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem` + +```shell +npm install @deepkit/filesystem +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/framework.md b/website/src/translations/ja/documentation/package/framework.md new file mode 100644 index 000000000..533f18213 --- /dev/null +++ b/website/src/translations/ja/documentation/package/framework.md @@ -0,0 +1,8 @@ +# API `@deepkit/framework` + +```shell +npm install @deepkit/framework +``` + + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/http.md b/website/src/translations/ja/documentation/package/http.md new file mode 100644 index 000000000..48f259e61 --- /dev/null +++ b/website/src/translations/ja/documentation/package/http.md @@ -0,0 +1,7 @@ +# API `@deepkit/http` + +```shell +npm install @deepkit/http +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/injector.md b/website/src/translations/ja/documentation/package/injector.md new file mode 100644 index 000000000..b7b6febab --- /dev/null +++ b/website/src/translations/ja/documentation/package/injector.md @@ -0,0 +1,7 @@ +# API `@deepkit/injector` + +```shell +npm install @deepkit/injector +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/logger.md b/website/src/translations/ja/documentation/package/logger.md new file mode 100644 index 000000000..224a2be7c --- /dev/null +++ b/website/src/translations/ja/documentation/package/logger.md @@ -0,0 +1,7 @@ +# API `@deepkit/logger` + +```sh +npm install @deepkit/logger +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/mongo.md b/website/src/translations/ja/documentation/package/mongo.md new file mode 100644 index 000000000..f19227d1a --- /dev/null +++ b/website/src/translations/ja/documentation/package/mongo.md @@ -0,0 +1,18 @@ +# API `@deepkit/mongo` + +```shell +npm install @deepkit/mongo +``` + +スタンドアロンの MongoDB ドライバーと Deepkit ORM 用のデータベースアダプターです。 + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/mysql.md b/website/src/translations/ja/documentation/package/mysql.md new file mode 100644 index 000000000..a972f372a --- /dev/null +++ b/website/src/translations/ja/documentation/package/mysql.md @@ -0,0 +1,17 @@ +# API `@deepkit/mysql` + +```shell +npm install @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('mysql://user:password@localhost/mydatabase'); +// const adapter = new MySQLDatabaseAdapter({host: 'localhost', port: 3306}); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/orm-browser.md b/website/src/translations/ja/documentation/package/orm-browser.md new file mode 100644 index 000000000..57d8aa1fa --- /dev/null +++ b/website/src/translations/ja/documentation/package/orm-browser.md @@ -0,0 +1,18 @@ +# Deepkit ORM Browser + +```sh +npm install @deepkit/orm-browser +``` + +Deepkit ORM Browser は、Database ORM schema を閲覧し、コンテンツを編集し、Migration の変更を確認し、データベースを seed できる Web アプリケーションです。 + +これは [Framework Debugger](../framework.md) の一部ですが、スタンドアロンでも利用できます。 + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/orm.md b/website/src/translations/ja/documentation/package/orm.md new file mode 100644 index 000000000..8ee0b58af --- /dev/null +++ b/website/src/translations/ja/documentation/package/orm.md @@ -0,0 +1,7 @@ +# API `@deepkit/orm` + +```shell +npm install @deepkit/orm +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/postgres.md b/website/src/translations/ja/documentation/package/postgres.md new file mode 100644 index 000000000..3f51e1f54 --- /dev/null +++ b/website/src/translations/ja/documentation/package/postgres.md @@ -0,0 +1,18 @@ +# API `@deepkit/postgres` + +```shell +npm install @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('postgres://user:password@localhost/mydatabase'); +// const adapter = new PostgresDatabaseAdapter({ host: 'localhost', database: 'postgres', user: 'postgres' }); + +const database = new Database(adapter); +``` + + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/rpc-tcp.md b/website/src/translations/ja/documentation/package/rpc-tcp.md new file mode 100644 index 000000000..7a980d719 --- /dev/null +++ b/website/src/translations/ja/documentation/package/rpc-tcp.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc-tcp` + +```shell +npm install @deepkit/rpc-tcp +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/rpc.md b/website/src/translations/ja/documentation/package/rpc.md new file mode 100644 index 000000000..a583f5fe4 --- /dev/null +++ b/website/src/translations/ja/documentation/package/rpc.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc` + +```shell +npm install @deepkit/rpc +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/run.md b/website/src/translations/ja/documentation/package/run.md new file mode 100644 index 000000000..bffd22eb8 --- /dev/null +++ b/website/src/translations/ja/documentation/package/run.md @@ -0,0 +1,21 @@ +# API `@deepkit/run` + +```sh +npm install @deepkit/run +``` + +ビルドステップを必要とせずに TypeScript コードを実行する簡単な方法。 + +このツールは主に Deepkit 独自のテストスイートでの使用を想定していますが、ご自身のプロジェクトでも使用できます。 + +```typescript +import { typeOf } from '@deepkit/type'; + +console.log(typeOf()); +``` + +```sh +node --import @deepkit/run test.ts +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/sql.md b/website/src/translations/ja/documentation/package/sql.md new file mode 100644 index 000000000..3207c73ed --- /dev/null +++ b/website/src/translations/ja/documentation/package/sql.md @@ -0,0 +1,7 @@ +# API `@deepkit/sql` + +```shell +npm install @deepkit/sql +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/sqlite.md b/website/src/translations/ja/documentation/package/sqlite.md new file mode 100644 index 000000000..5486e2c82 --- /dev/null +++ b/website/src/translations/ja/documentation/package/sqlite.md @@ -0,0 +1,16 @@ +# API `@deepkit/sqlite` + +```shell +npm install @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { Database } from '@deepkit/orm'; + +const adapter = new SQLiteDatabaseAdapter(':memory'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/stopwatch.md b/website/src/translations/ja/documentation/package/stopwatch.md new file mode 100644 index 000000000..d2a69ed13 --- /dev/null +++ b/website/src/translations/ja/documentation/package/stopwatch.md @@ -0,0 +1,7 @@ +# API `@deepkit/stopwatch` + +```shell +npm install @deepkit/stopwatch +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/template.md b/website/src/translations/ja/documentation/package/template.md new file mode 100644 index 000000000..e6d908c2b --- /dev/null +++ b/website/src/translations/ja/documentation/package/template.md @@ -0,0 +1,7 @@ +# API `@deepkit/template` + +```shell +npm install @deepkit/template +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/topsort.md b/website/src/translations/ja/documentation/package/topsort.md new file mode 100644 index 000000000..cbeddd4c2 --- /dev/null +++ b/website/src/translations/ja/documentation/package/topsort.md @@ -0,0 +1,7 @@ +# API `@deepkit/topsort` + +```shell +npm install @deepkit/topsort +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/type-compiler.md b/website/src/translations/ja/documentation/package/type-compiler.md new file mode 100644 index 000000000..e50fc4f33 --- /dev/null +++ b/website/src/translations/ja/documentation/package/type-compiler.md @@ -0,0 +1,9 @@ +# API `@deepkit/type-compiler` + +```shell +npm install @deepkit/type-compiler +``` + +実行時の型情報を利用可能にするための TypeScript の transformer です。 + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/type.md b/website/src/translations/ja/documentation/package/type.md new file mode 100644 index 000000000..195e08585 --- /dev/null +++ b/website/src/translations/ja/documentation/package/type.md @@ -0,0 +1,7 @@ +# API `@deepkit/type` + +```shell +npm install @deepkit/type +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/vite.md b/website/src/translations/ja/documentation/package/vite.md new file mode 100644 index 000000000..3f1bcdd0a --- /dev/null +++ b/website/src/translations/ja/documentation/package/vite.md @@ -0,0 +1,7 @@ +# API `@deepkit/vite` + +```shell +npm install @deepkit/vite +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/package/workflow.md b/website/src/translations/ja/documentation/package/workflow.md new file mode 100644 index 000000000..ff27914f7 --- /dev/null +++ b/website/src/translations/ja/documentation/package/workflow.md @@ -0,0 +1,7 @@ +# API `@deepkit/workflow` + +```shell +npm install @deepkit/workflow +``` + + \ No newline at end of file diff --git a/website/src/translations/ja/documentation/rpc.md b/website/src/translations/ja/documentation/rpc.md new file mode 100644 index 000000000..591081069 --- /dev/null +++ b/website/src/translations/ja/documentation/rpc.md @@ -0,0 +1,64 @@ +# RPC + +RPC は Remote Procedure Call(リモートプロシージャコール)の略で、リモートサーバー上の関数をローカル関数のように呼び出せる仕組みです。マッピングに HTTP メソッドと URL を用いる HTTP のクライアント-サーバー通信とは異なり、RPC は関数名でマッピングを行います。送信するデータは通常の関数の引数として渡され、サーバーでの関数呼び出しの結果がクライアントへ返送されます。 + +RPC の利点は、ヘッダー、URL、クエリ文字列などを扱わないため、クライアント-サーバーの抽象化が軽量であることです。欠点は、RPC 経由でのサーバー上の関数はブラウザから簡単には呼び出せず、しばしば特定のクライアントを必要とすることです。 + +RPC の重要な特徴のひとつは、クライアントとサーバー間のデータが自動的にシリアライズ/デシリアライズされることです。そのため、型安全な RPC クライアントを実現しやすくなります。いくつかの RPC フレームワークは、特定の形式で型(Parameter の型や Return Value)を提供することをユーザーに強制します。これは gRPC の Protocol Buffers や GraphQL のような DSL、あるいは JavaScript のスキーマビルダーの形を取る場合があります。追加のデータ検証も RPC フレームワークが提供できることがありますが、すべてでサポートされているわけではありません。 + +Deepkit RPC は TypeScript のコードそのものから Type を抽出するため、コードジェネレーターを使用したり手動で定義したりする必要がありません。Deepkit は Parameter と結果の自動シリアライズ/デシリアライズをサポートします。Validation に追加の制約が定義されると、それらは自動的に検証されます。これにより、RPC による通信は非常に型安全で効率的になります。Deepkit RPC の `rxjs` によるストリーミングのサポートは、この RPC フレームワークをリアルタイム通信に適したツールにします。 + +RPC の背後にある概念を示すため、次のコードを考えてみましょう: + +```typescript +//server.ts +class Controller { + hello(title: string): string { + return 'Hello ' + title + } +} +``` + +hello のような Method は、サーバー上の Class 内で通常の Function と同様に実装され、リモートクライアントから呼び出すことができます。 + +```typescript +//client.ts +const client = new RpcClient('localhost'); +const controller = client.controller(); + +const result = await controller.hello('World'); // => 'Hello World'; +``` + +RPC は本質的に非同期通信に基づいているため、通信は通常 HTTP を介して行われますが、TCP や WebSocket を介して行うこともできます。これは、TypeScript のすべての関数呼び出しがそれ自体 `Promise` に変換されることを意味します。結果は対応する `await` により非同期に受け取れます。 + +## アイソモーフィック TypeScript + +プロジェクトがクライアント(通常はフロントエンド)とサーバー(バックエンド)の両方で TypeScript を使用している場合、これをアイソモーフィック TypeScript と呼びます。TypeScript の Type に基づく型安全な RPC フレームワークは、このようなプロジェクトに特に有用で、クライアントとサーバーの間で型を共有できます。 + +これを活用するには、両側で使用される型を専用のファイルまたはパッケージに切り出すとよいでしょう。それぞれの側でインポートすることで、再び組み合わせて利用できます。 + +```typescript +//shared.ts +export class User { + id: number; + username: string; +} + +//server.ts +import { User } from './shared'; + +@rpc.controller('/user') +class UserController { + async getUser(id: number): Promise { + return await datbase.query(User).filter({id}).findOne(); + } +} + +//client.ts +import { UserControllerApi } from './shared'; +import type { UserController } from './server.ts' +const controller = client.controller('/user'); +const user = await controller.getUser(2); // => User +``` + +後方互換性は、通常のローカル API と同様の方法で実現できます。新しい Parameter をオプショナルとしてマークするか、新しい Method を追加します。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/rpc/dependency-injection.md b/website/src/translations/ja/documentation/rpc/dependency-injection.md new file mode 100644 index 000000000..1c84698e0 --- /dev/null +++ b/website/src/translations/ja/documentation/rpc/dependency-injection.md @@ -0,0 +1,55 @@ +# 依存性注入 + +Controller クラスは `@deepkit/injector` の依存性注入コンテナによって管理されます。Deepkit Framework を使用する場合、これらのコントローラは、そのコントローラを提供するモジュールのプロバイダーに自動的にアクセスできます。 + +Deepkit Framework では、コントローラは依存性注入スコープ `rpc` でインスタンス化され、すべてのコントローラはこのスコープのさまざまなプロバイダーに自動的にアクセスできます。これらの追加のプロバイダーは `HttpRequest`(任意)、`RpcInjectorContext`、`SessionState`、`RpcKernelConnection`、`ConnectionWriter` です。 + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { App } from '@deepkit/app'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +new App({ + providers: [{ provide: Database, useValue: new Database }] + controllers: [Controller], +}).run(); +``` + +ただし、`RpcKernel` を手動でインスタンス化する場合、DI コンテナを渡すこともできます。すると RPC コントローラはこの DI コンテナを通じてインスタンス化されます。これは、Express.js のような Deepkit Framework 以外の環境で `@deepkit/rpc` を使用したい場合に有用です。 + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { InjectorContext } from '@deepkit/injector'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +const injector = InjectorContext.forProviders([ + Controller, + { provide: Database, useValue: new Database }, +]); +const kernel = new RpcKernel(injector); +kernel.registerController(Controller); +``` + +詳しくは [依存性注入](../dependency-injection.md) を参照してください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/rpc/errors.md b/website/src/translations/ja/documentation/rpc/errors.md new file mode 100644 index 000000000..2d762ff15 --- /dev/null +++ b/website/src/translations/ja/documentation/rpc/errors.md @@ -0,0 +1,55 @@ +# エラー + +throw された Error は、エラーメッセージやスタックトレースなどの情報とともに自動的にクライアントへ転送されます。 + +Error オブジェクトの nominal なインスタンスが重要(`instanceof` を使用するため)な場合は、指定の Error Class が runtime に登録され再利用されるように、`@entity.name('@error:unique-name')` を使用する必要があります。 + +```typescript +@entity.name('@error:myError') +class MyError extends Error {} + +// サーバー +@rpc.controller('/main') +class Controller { + @rpc.action() + saveUser(user: User): void { + throw new MyError('Can not save user'); + } +} + +// クライアント +// [MyError] により、Class MyError が runtime で認識されることを保証します +const controller = client.controller('/main', [MyError]); + +try { + await controller.getUser(2); +} catch (e) { + if (e instanceof MyError) { + // おっと、ユーザーを保存できませんでした + } else { + // その他すべてのエラー + } +} +``` + +## Error の変換 + +throw された Error はエラーメッセージやスタックトレースなどの情報とともに自動的にクライアントへ転送されるため、意図せず機密情報を公開してしまう可能性があります。これを変更するには、Method `transformError` で throw された Error を修正できます。 + +```typescript +class MyKernelSecurity extends RpcKernelSecurity { + constructor(private logger: Logger) { + super(); + } + + transformError(error: Error) { + // 新しい Error でラップする + this.logger.error('Error in RPC', error); + return new Error('Something went wrong: ' + error.message); + } +} +``` + +一度エラーが汎用的な `error` に変換されると、完全なスタックトレースと Error の同一性は失われます。したがって、クライアント側のエラーに対して `instanceof` チェックは使用できません。 + +Deepkit RPC が 2 つのマイクロサービス間で使用され、クライアントとサーバーが開発者の完全な管理下にある場合、Error の変換が必要になることは稀です。一方、クライアントが不特定のユーザーのブラウザ上で動作している場合には、`transformError` においてどの情報を公開するかに注意する必要があります。迷う場合は、内部の詳細が漏洩しないように、各 Error を汎用的な `Error` に変換すべきです。その際、Error をログに記録するのがよいでしょう。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/rpc/getting-started.md b/website/src/translations/ja/documentation/rpc/getting-started.md new file mode 100644 index 000000000..c6c8b0c07 --- /dev/null +++ b/website/src/translations/ja/documentation/rpc/getting-started.md @@ -0,0 +1,147 @@ +# はじめに + +Deepkit RPC を使用するには、ランタイム型に基づいているため `@deepkit/type` を正しくインストールしておく必要があります。 [ランタイム型のインストール](../runtime-types.md) を参照してください。 + +これが完了したら、ライブラリを内部で既に使用している `@deepkit/rpc` または Deepkit Framework をインストールできます。 + +```sh +npm install @deepkit/rpc +``` + +なお、`@deepkit/rpc` の Controller クラスは TypeScript のデコレーターに基づいており、この機能を使用するには experimentalDecorators を有効にする必要があります。 + +サーバーとクライアントがそれぞれ独自の package.json を持つ場合は、`@deepkit/rpc` パッケージを両方にインストールする必要があります。 + +サーバーと TCP で通信するには、クライアントとサーバーの両方に `@deepkit/rpc-tcp` パッケージをインストールする必要があります。 + +```sh +npm install @deepkit/rpc-tcp +``` + +WebSocket 通信を行う場合、サーバー側にもこのパッケージが必要です。一方で、ブラウザー内のクライアントは標準の WebSocket を使用します。 + +クライアントを WebSocket が利用できない環境(例: NodeJS)でも使用する場合は、クライアント側に ws パッケージが必要です。 + +```sh +npm install ws +``` + +## 使い方 + +以下は、WebSocket と @deepkit/rpc の低レベル API に基づく完全に動作するサンプルです。Deepkit Framework を使用する場合、Controller はアプリのモジュール経由で提供され、RpcKernel を手動でインスタンス化する必要はありません。 + +_ファイル: server.ts_ + +```typescript +import { rpc, RpcKernel } from '@deepkit/rpc'; +import { RpcWebSocketServer } from '@deepkit/rpc-tcp'; + +@rpc.controller('/main') +export class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } +} + +const kernel = new RpcKernel(); +kernel.registerController(Controller); +const server = new RpcWebSocketServer(kernel, 'localhost:8081'); +server.start({ + host: '127.0.0.1', + port: 8081, +}); +console.log('Server started at ws://127.0.0.1:8081'); + +``` + +_ファイル: client.ts_ + +```typescript +import { RpcWebSocketClient } from '@deepkit/rpc'; +import type { Controller } from './server'; + +async function main() { + const client = new RpcWebSocketClient('ws://127.0.0.1:8081'); + const controller = client.controller('/main'); + + const result = await controller.hello('World'); + console.log('result', result); + + client.disconnect(); +} + +main().catch(console.error); + +``` + +## サーバーの Controller + +Remote Procedure Call における「Procedure」という用語は、「Action」とも一般的に呼ばれます。Action はクラス内で定義されたメソッドで、`@rpc.action` デコレーターでマークされます。クラス自体は `@rpc.controller` デコレーターで Controller としてマークされ、一意の名前が与えられます。この名前は、クライアントで正しい Controller にアクセスするために参照されます。必要に応じて複数の Controller を定義して登録できます。 + + +```typescript +import { rpc } from '@deepkit/rpc'; + +@rpc.controller('/main'); +class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } + + @rpc.action() + test(): boolean { + return true; + } +} +``` + +クライアントから呼び出せるのは、`@rpc.action()` が付与されたメソッドのみです。 + +型は明示的に指定しなければならず、型推論は使用できません。これは、シリアライザーがデータをバイナリ(BSON)や JSON に変換して送信するために、型の構造を正確に把握する必要があるためです。 + +## クライアントの Controller + +RPC の一般的な流れでは、クライアントがサーバー上の関数を実行します。しかし Deepkit RPC では、サーバーがクライアント上の関数を実行することも可能です。これを実現するために、クライアント側でも Controller を登録できます。 + +TODO + +## 依存性注入 + +Deepkit Framework を使用する場合、クラスは依存性注入コンテナによってインスタンス化され、アプリケーション内の他のすべてのプロバイダーに自動的にアクセスできます。 + +あわせて [依存性注入](dependency-injection.md#) も参照してください。 + +## RxJS のストリーミング + +TODO + +## 名目型 + +クライアントが関数呼び出しのデータを受け取る際、そのデータはまずサーバーでシリアライズされ、クライアントでデシリアライズされます。関数の戻り値の型にクラスが含まれている場合、これらのクラスはクライアント側で再構築されますが、名目上の同一性や関連するメソッドを失います。この問題に対処するには、クラスに一意の ID/名前を付けて名目型として登録してください。この手法は、RPC-API 内で使用されるすべてのクラスに適用するべきです。 + +クラスを登録するには、`@entity.name('id')` デコレーターを使用します。 + +```typescript +import { entity } from '@deepkit/type'; + +@entity.name('user') +class User { + id!: number; + firstName!: string; + lastName!: string; + get fullName() { + return this.firstName + ' ' + this.lastName; + } +} +``` + +このクラスが関数の結果として使用されると、その同一性が保持されます。 + +```typescript +const controller = client.controller('/main'); + +const user = await controller.getUser(2); +user instanceof User; // @entity.name が使用されていれば true、そうでなければ false +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/rpc/security.md b/website/src/translations/ja/documentation/rpc/security.md new file mode 100644 index 000000000..7d7980c54 --- /dev/null +++ b/website/src/translations/ja/documentation/rpc/security.md @@ -0,0 +1,131 @@ +# セキュリティ + +デフォルトでは、すべての RPC 関数は任意のクライアントから呼び出すことができ、ピア・ツー・ピア通信機能が有効になっています。どのクライアントに何を許可するかを厳密に制御するには、`RpcKernelSecurity` クラスをオーバーライドできます。 + +```typescript +import { RpcKernelSecurity, Session, RpcControllerAccess } from '@deepkit/type'; + +//デフォルトの実装を含みます +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + return true; + } + + async isAllowedToRegisterAsPeer(session: Session, peerId: string): Promise { + return true; + } + + async isAllowedToSendToPeer(session: Session, peerId: string): Promise { + return true; + } + + async authenticate(token: any): Promise { + throw new Error('Authentication not implemented'); + } + + transformError(err: Error) { + return err; + } +} +``` + +これを使用するには、`RpcKernel` にプロバイダを渡します: + +```typescript +const kernel = new RpcKernel([{provide: RpcKernelSecurity, useClass: MyKernelSecurity, scope: 'rpc'}]); +``` + +また、Deepkit アプリの場合は、アプリ内でプロバイダを使って `RpcKernelSecurity` クラスをオーバーライドします: + +```typescript +import { App } from '@deepkit/type'; +import { RpcKernelSecurity } from '@deepkit/rpc'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + controllers: [MyRpcController], + providers: [ + {provide: RpcKernelSecurity, useClass: MyKernelSecurity, scope: 'rpc'} + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 認証 / セッション + +デフォルトでは、`Session` オブジェクトは匿名セッションであり、クライアントが認証されていないことを意味します。クライアントが認証したい場合、`authenticate` メソッドが呼び出されます。`authenticate` メソッドが受け取るトークンはクライアントから送られ、任意の値を取り得ます。 + +クライアントがトークンを設定すると、最初の RPC 関数が呼び出されたとき、または `client.connect()` が手動で実行されたときに認証が行われます。 + +```typescript +const client = new RpcWebSocketClient('localhost:8081'); +client.token.set('123456789'); + +const controller = client.controller('/main'); +``` + +この場合、`RpcKernelSecurity.authenticate` はトークン `123456789` を受け取り、それに応じて別のセッションを返すことができます。返されたセッションは、その後 `hasControllerAccess` のような他のすべてのメソッドに渡されます。 + +```typescript +import { Session, RpcKernelSecurity } from '@deepkit/rpc'; + +class UserSession extends Session { +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.controllerClassType instanceof MySecureController) { + //MySecureController には UserSession が必要です + return session instanceof UserSession; + } + return true; + } + + async authenticate(token: any): Promise { + if (token === '123456789') { + //username は ID でもユーザー名でもかまいません + return new UserSession('username', token); + } + throw new Error('Authentication failed'); + } +} +``` + +## コントローラーアクセス + +`hasControllerAccess` メソッドは、クライアントが特定の RPC 関数を実行することを許可されているかどうかを判定します。このメソッドは、すべての RPC 関数呼び出しに対して呼び出されます。`false` を返した場合、アクセスは拒否され、クライアント側でエラーが送出されます。 + +`RpcControllerAccess` には RPC 関数に関する有用な情報が含まれます: + +```typescript +interface RpcControllerAccess { + controllerName: string; + controllerClassType: ClassType; + actionName: string; + actionGroups: string[]; + actionData: { [name: string]: any }; +} +``` + +グループと追加データはデコレーター `@rpc.action()` を介して変更できます: + +```typescript +class Controller { + @rpc.action().group('secret').data('role', 'admin') + saveUser(user: User): void { + } +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.actionGroups.includes('secret')) { + if (session instanceof UserSession) { + //todo: 確認 + return session.username === 'admin'; + } + return false; + } + return true; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/rpc/transport.md b/website/src/translations/ja/documentation/rpc/transport.md new file mode 100644 index 000000000..c4b7c68ca --- /dev/null +++ b/website/src/translations/ja/documentation/rpc/transport.md @@ -0,0 +1,17 @@ +# トランスポートプロトコル + +Deepkit RPC は複数のトランスポートプロトコルをサポートします。WebSocket は(ブラウザがサポートしているため)最も互換性が高く、ストリーミングなどのすべての機能もサポートするプロトコルです。TCP は通常、高速で、サーバー間(マイクロサービス)や非ブラウザクライアントとの通信に最適です。ただし、WebSocket もサーバー間通信でうまく機能します。 + +## HTTP + +Deepkit の RPC HTTP プロトコルは、各関数呼び出しが HTTP リクエストであるため、ブラウザでのデバッグが特に容易なバリアントですが、RxJS ストリーミングをサポートしないなどの制限があります。 + +TODO: まだ実装されていません。 + +## WebSocket + +@deepkit/rpc-tcp `RpcWebSocketServer` と ブラウザの WebSocket または Node の `ws` パッケージ。 + +## TCP + +@deepkit/rpc-tcp `RpcNetTcpServer` と `RpcNetTcpClientAdapter` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types.md b/website/src/translations/ja/documentation/runtime-types.md new file mode 100644 index 000000000..905a3fd6c --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types.md @@ -0,0 +1,9 @@ +# ランタイム型 + +TypeScript におけるランタイム型情報は、これまで利用できなかった、または回避策を必要とした新しいワークフローや機能を解放します。現代の開発プロセスは、GraphQL、バリデータ、ORM、ProtoBuf のようなエンコーダーといったツールのために、型やスキーマを宣言することに大きく依存しています。これらのツールでは、ProtoBuf や GraphQL が独自の宣言言語を持っていたり、バリデータが独自のスキーマ API や JSON-Schema を使用していたりと、ユースケースに特化した新しい言語を学ぶ必要がある場合があります。 + +TypeScript は複雑な構造を表現できるほど強力になっており、GraphQL、ProtoBuf、JSON-Schema のような宣言フォーマットを完全に置き換えることさえ可能です。ランタイム型システムがあれば、コードジェネレーターや「Zod」のようなランタイムの JavaScript 型宣言ライブラリを一切使わずに、これらのツールのユースケースをカバーできます。Deepkit ライブラリは、ランタイム型情報を提供し、効率的で互換性のあるソリューションの開発を容易にすることを目指しています。 + +Deepkit は、ランタイムで型情報を読み取る能力に基づいて構築されており、効率のために可能な限り多くの TypeScript の型情報を利用します。ランタイム型システムにより、クラスのプロパティ、関数のパラメータ、返り値の型といった動的な型を読み取り、計算できます。Deepkit は TypeScript のコンパイルプロセスにフックし、[カスタムのバイトコードと仮想マシン](https://github.com/microsoft/TypeScript/issues/47658)を用いて、すべての型情報が生成された JavaScript に埋め込まれることを保証し、開発者がプログラム的に型情報へアクセスできるようにします。 + +Deepkit を使えば、開発者は既存の TypeScript の型をランタイムでのバリデーション、シリアライズなどに活用でき、開発プロセスを簡素化し、作業をより効率的にできます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/bytecode.md b/website/src/translations/ja/documentation/runtime-types/bytecode.md new file mode 100644 index 000000000..6e4eeb239 --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/bytecode.md @@ -0,0 +1,76 @@ +# バイトコード + +JavaScript における Deepkit の型情報のエンコードと読み取りの詳細を学ぶために、この章があります。ここでは、型が実際にどのようにバイトコードへ変換され、JavaScript に出力され、実行時に解釈されるかを説明します。 + +## Typen-Compiler + +型コンパイラ(@deepkit/type-compiler)は、TypeScript ファイル内で定義された型を読み取り、バイトコードにコンパイルする役割を担います。このバイトコードには、実行時に型を実行するために必要なものがすべて含まれています。 +執筆時点では、型コンパイラはいわゆる TypeScript トランスフォーマーです。このトランスフォーマーは TypeScript コンパイラ自体のプラグインであり、TypeScript の AST(抽象構文木)を別の TypeScript AST に変換します。Deepkit の型コンパイラはこの過程で AST を読み取り、対応するバイトコードを生成し、それを AST に挿入します。 + +TypeScript 自体はこのプラグイン(トランスフォーマー)を tsconfig.json で設定することを許可していません。TypeScript コンパイラ API を直接使うか、`ts-loader` を備えた Webpack などのビルドシステムを使う必要があります。Deepkit ユーザーがこの不便な方法を避けられるように、Deepkit の型コンパイラは `@deepkit/type-compiler` がインストールされると `node_modules/typescript` に自動的にインストールされます。これにより、ローカルにインストールされた TypeScript(`node_modules/typescript` のもの)へアクセスするすべてのビルドツールで型コンパイラが自動的に有効になります。その結果、tsc、Angular、webpack、ts-node、その他いくつかのツールが Deepkit の型コンパイラと自動的に連携して動作します。 + +NPM のインストールスクリプトの自動実行が無効で、ローカルにインストールされた TypeScript が変更されない場合は、この処理を必要に応じて手動で実行する必要があります。あるいは、webpack のようなビルドツールで型コンパイラを手動で使用することもできます。上記のインストールセクションを参照してください。 + +## バイトコードのエンコーディング + +バイトコードは仮想マシンのためのコマンド列であり、JavaScript 自体の中に参照と文字列(実際のバイトコード)の配列としてエンコードされます。 + +```typescript +//TypeScript +type TypeA = string; + +//生成された JavaScript +const typeA = ['&']; +``` + +既存のコマンド自体はそれぞれ 1 バイトのサイズで、`@deepkit/type-spec` の `ReflectionOp` enum として定義されています。執筆時点では、コマンドセットは 81 を超えるコマンドで構成されています。 + +```typescript +enum ReflectionOp { + never, + any, + unknown, + void, + object, + + string, + number, + + //...さらに多数 +} +``` + +コマンドのシーケンスはメモリ節約のため文字列としてエンコードされます。したがって、型 `string[]` はバイトコードプログラム `[string, array]` として概念化され、バイト列 `[5, 37]` を持ち、次のアルゴリズムでエンコードされます。 + +```typescript +function encodeOps(ops: ReflectionOp[]): string { + return ops.map(v => String.fromCharCode(v + 33)).join(''); +} +``` + +それに応じて、5 は `&` 文字になり、37 は `F` 文字になります。合わせて `&F` となり、Javascript では `['&F']` として出力されます。 + +```typescript +//TypeScript +export type TypeA = string[]; + +//生成された JavaScript +export const __ΩtypeA = ['&F']; +``` + +名前の衝突を防ぐため、各型には "_Ω" プレフィックスが付与されます。エクスポートされた型、またはエクスポートされた型で使用される明示的に定義された各型について、JavaScript にバイトコードが出力されます。クラスや関数も、プロパティとして直接バイトコードを受け取ります。 + +```typescript +//TypeScript +function log(message: string): void {} + +//生成された JavaScript +function log(message) {} +log.__type = ['message', 'log', 'P&2!$/"']; +``` + +## 仮想マシン + +実行時には仮想マシン(`@deepkit/type` の Class Processor)が、エンコードされたバイトコードのデコードと実行を担当します。常に [リフレクション API](./reflection.md) で説明されている型オブジェクトを返します。 + +詳しくは [TypeScript バイトコードインタプリタ / 実行時の型 #47658](https://github.com/microsoft/TypeScript/issues/47658) を参照してください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/custom-serializer.md b/website/src/translations/ja/documentation/runtime-types/custom-serializer.md new file mode 100644 index 000000000..97ae8e15a --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/custom-serializer.md @@ -0,0 +1,99 @@ +# カスタムシリアライザ + +デフォルトでは、`@deepkit/type` には JSON シリアライザと TypeScript の型検証が付属しています。これを拡張してシリアライズ機能を追加・削除したり、検証の方法を変更することができます。検証はシリアライザとも連動しています。 + +## 新しいシリアライザ + +シリアライザは、シリアライザ テンプレートが登録された `Serializer` Class の単なるインスタンスです。シリアライザ テンプレートは、JIT シリアライザ処理のための JavaScript コードを生成する小さな Function です。各型(String、Number、Boolean など)ごとに、データ変換や検証のためのコードを返す専用のシリアライザ テンプレートが用意されます。このコードは、ユーザーが使用している JavaScript エンジンと互換である必要があります。 + +コンパイラ テンプレート関数の実行時にのみ、完全な型情報に(もしくはそうあるべきように)フルアクセスできます。型を変換するために必要な情報をすべて直接 JavaScript コードに埋め込むことで、高度に最適化されたコード(JIT 最適化コードとも呼ばれます)を得る、というのが狙いです。 + +次の例では空のシリアライザを作成します。 + +```typescript +import { EmptySerializer } from '@deepkit/type'; + +class User { + name: string = ''; + created: Date = new Date; +} + +const mySerializer = new EmptySerializer('mySerializer'); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 0 } +``` + +ご覧の通り、何も変換されていません(`created` は依然として数値ですが、`Date` として定義しています)。これを変更するため、Date 型のデシリアライズ用シリアライザ テンプレートを追加します。 + +```typescript +mySerializer.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); +}); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 2021-06-10T19:34:27.301Z } +``` + +これでシリアライザは値を Date オブジェクトに変換します。 + +シリアライズでも同様にするため、別のシリアライズ用テンプレートを登録します。 + +```typescript +mySerializer.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); +}); + +const user1 = new User(); +user1.name = 'Peter'; +user1.created = new Date('2021-06-10T19:34:27.301Z'); +console.log(serialize(user1, undefined, mySerializer)); +``` + +```sh +{ name: 'Peter', created: '2021-06-10T19:34:27.301Z' } +``` + +新しいシリアライザは、シリアライズ処理で Date オブジェクトの日付を文字列に正しく変換します。 + +## 例 + +さらに多くの例については、Deepkit Type に含まれている [JSON シリアライザ](https://github.com/deepkit/deepkit-framework/blob/master/packages/type/src/serializer.ts#L1688) のコードを参照してください。 + +## 既存のシリアライザの拡張 + +既存のシリアライザを拡張したい場合は、Class 継承を使って実現できます。これは、シリアライザが constructor でテンプレートを登録するように作られているため機能します。 + +```typescript +class MySerializer extends Serializer { + constructor(name: string = 'mySerializer') { + super(name); + this.registerTemplates(); + } + + protected registerTemplates() { + this.deserializeRegistry.register(ReflectionKind.string, (type, state) => { + state.addSetter(`String(${state.accessor})`); + }); + + this.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); + }); + + this.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); + }); + } +} +const mySerializer = new MySerializer(); +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/extend.md b/website/src/translations/ja/documentation/runtime-types/extend.md new file mode 100644 index 000000000..b69b9c9a4 --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/extend.md @@ -0,0 +1,56 @@ +# 拡張 + +## カスタムシリアライズ + +独自の `Serializer` を作成するか、既定の `serializer` を拡張することで、型のシリアライズを拡張できます。 + +この例では、Class `Point` をタプル `[number, number]` にシリアライズ/デシリアライズする方法を示します。 + +```typescript +import { serializer, SerializationError } from '@deepkit/type'; + +class Point { + constructor(public x: number, public y: number) { + } +} + +// deserialize は JSON から(Class)インスタンスへの変換を意味します。 +serializer.deserializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: any) => { + // すでに Point インスタンスであれば、処理は完了です + if (v instanceof Point) return v; + + // この時点で `v` は(undefined 以外の)任意の値であり得るため、検証が必要です + if (!Array.isArray(v)) throw new SerializationError('Expected array'); + if (v.length !== 2) throw new SerializationError('Expected array with two elements'); + if (typeof v[0] !== 'number' || typeof v[1] !== 'number') throw new SerializationError('Expected array with two numbers'); + return new Point(v[0], v[1]); + }); +}); + +serializer.serializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: Point) => { + // この時点では `v` は常に Point インスタンスです + return [v.x, v.y]; + }); +}); + +// cast と deserialize は既定で `serializer` を使用します +const point = cast([1, 2], undefined, serializer); +expect(point).toBeInstanceOf(Point); +expect(point.x).toBe(1); +expect(point.y).toBe(2); + +{ + expect(() => deserialize(['vbb'])).toThrowError(SerializationError); + expect(() => deserialize(['vbb'])).toThrow('Expected array with two elements') +} + +// serialize は既定で `serializer` を使用します +const json = serialize(point); +expect(json).toEqual([1, 2]); +``` + +これは `cast`、`deserialize`、`serialize` など、通常の `@deepkit/type` の Function に対してのみ機能する点に注意してください。 + +これはデータベース層には適用されません。データベース層は、マイグレーションやシリアライズ(例: BSON のシリアライズ)のために、Entity Class で定義された Type を使用するためです。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/external-types.md b/website/src/translations/ja/documentation/runtime-types/external-types.md new file mode 100644 index 000000000..5aeb1cbc5 --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/external-types.md @@ -0,0 +1,49 @@ +# 外部の型 + +## 外部のクラス + +TypeScript はデフォルトでは型情報を含まないため、他のパッケージからインポートされた型/クラス(@deepkit/type-compiler を使用していないもの)には型情報がありません。 + +外部クラスに型を注釈付けするには `annotateClass` を使用し、インポートしたクラスがどこかで使用される前に、この関数がアプリケーションのブートストラップ段階で実行されるようにしてください。 + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +interface AnnotatedClass { + id: number; + title: string; +} + +annotateClass(MyExternalClass); + +//MyExternalClass のすべての使用箇所は、現在 AnnotatedClass の型を返します +serialize({...}); + +//MyExternalClass は他の型でも使用できるようになりました +interface User { + id: number; + clazz: MyExternalClass; +} +``` + +これで `MyExternalClass` はシリアライズ関数やリフレクション API で使用できます。 + +次の例はジェネリック クラスに注釈を付ける方法を示します: + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +class AnnotatedClass { + id!: T; +} + +annotateClass(ExternalClass, AnnotatedClass); +``` + +## import type + +`import type` 構文は、実際の JavaScript コードをインポートせず、型チェックのためだけに使用できるようにするために TypeScript によって設計されています。これは、例えば実行時には存在せずコンパイル時にしか利用できないパッケージの型を使いたい場合や、実行時にそのパッケージを実際に読み込みたくない場合に有用です。 + +Deepkit は `import type` の思想を尊重し、実行時コードを一切生成しません。つまり、`import type` を使用した場合、実行時には型情報は利用できないということです。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/getting-started.md b/website/src/translations/ja/documentation/runtime-types/getting-started.md new file mode 100644 index 000000000..60a103d0c --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/getting-started.md @@ -0,0 +1,105 @@ +# はじめに + +Deepkit のランタイム型システムをインストールするには、Deepkit Type Compiler と Deepkit Type パッケージ本体という 2 つのパッケージが必要です。Type Compiler は、TypeScript の型からランタイム型情報を生成する TypeScript のトランスフォーマーです。Type パッケージには、ランタイム仮想マシンや型アノテーションに加えて、型を扱うための多くの便利な関数が含まれます。 + +## インストール + +```sh +npm install --save @deepkit/type +npm install --save-dev @deepkit/type-compiler typescript ts-node +``` + +ランタイム型情報はデフォルトでは生成されません。有効化するには、`tsconfig.json` ファイルで `"reflection": true` を設定する必要があります。 + +デコレーターを使用する場合は、`tsconfig.json` で `"experimentalDecorators": true` を有効にする必要があります。これは `@deepkit/type` を使うために厳密には必須ではありませんが、他の Deepkit ライブラリや Deepkit Framework の特定の機能では必要です。 + +_ファイル: tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +ランタイム型情報を使った最初のコードを書いてみましょう: + +_ファイル: app.ts_ + +```typescript +import { cast, MinLength, ReflectionClass } from '@deepkit/type'; + +interface User { + username: string & MinLength<3>; + birthDate?: Date; +} + +const user = cast({ + username: 'Peter', + birthDate: '2010-10-10T00:00:00Z' +}); +console.log(user); + +const reflection = ReflectionClass.from(); +console.log(reflection.getProperty('username').type); +``` + +そして `ts-node` で実行します: + +```sh +./node_modules/.bin/ts-node app.ts +``` + +## インタラクティブな例 + +こちらに CodeSandbox の例があります: https://codesandbox.io/p/sandbox/deepkit-runtime-types-fjmc2f?file=index.ts + +## 型コンパイラ + +TypeScript 自体は、`tsconfig.json` を通じて型コンパイラを構成することを許可していません。そのため、TypeScript Compiler API を直接使用するか、_ts-loader_ を備えた Webpack のようなビルドシステムを使う必要があります。Deepkit の利用者がこの不便さを回避できるよう、`@deepkit/type-compiler` をインストールすると、Deepkit の型コンパイラは自動的に `node_modules/typescript` に自身をインストールします(これは NPM のインストールフックによって行われます)。 +これにより、ローカルにインストールされた TypeScript(`node_modules/typescript` 内のもの)にアクセスするすべてのビルドツールで、型コンパイラが自動的に有効になります。これによって、_tsc_、Angular、webpack、_ts-node_ などのツールが、Deepkit の型コンパイラと自動的に連携して動作します。 + +型コンパイラが自動的に正しくインストールされなかった場合(たとえば NPM のインストールフックが無効化されている場合)、次のコマンドで手動インストールできます: + +```sh +node_modules/.bin/deepkit-type-install +``` + +ローカルの TypeScript のバージョンが更新された場合(たとえば、package.json の typescript のバージョンが変更されて `npm install` を実行した場合など)は、`deepkit-type-install` を実行する必要がある点に注意してください。 + +## Webpack + +webpack ビルドで型コンパイラを使用したい場合は、`ts-loader` パッケージ(またはトランスフォーマー登録をサポートする他の TypeScript ローダー)で設定できます。 + +_ファイル: webpack.config.js_ + +```javascript +const typeCompiler = require('@deepkit/type-compiler'); + +module.exports = { + entry: './app.ts', + module: { + rules: [ + { + test: /\.tsx?$/, + use: { + loader: 'ts-loader', + options: { + // これは @deepkit/type の型コンパイラを有効にします + getCustomTransformers: (program, getProgram) => ({ + before: [typeCompiler.transformer], + afterDeclarations: [typeCompiler.declarationTransformer], + }), + } + }, + exclude: /node_modules/, + }, + ], + }, +} +``` \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/reflection.md b/website/src/translations/ja/documentation/runtime-types/reflection.md new file mode 100644 index 000000000..7625fa9b8 --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/reflection.md @@ -0,0 +1,288 @@ +# リフレクション + +型情報自体を直接扱うには、基本的に2つの方式があります: Type オブジェクトと Reflection クラス。Type オブジェクトは `typeOf()` が返す通常の JS オブジェクトです。Reflection クラスについては以下で説明します。 + +`typeOf` 関数は、Interface、オブジェクトリテラル、Class、Function、型エイリアスを含むすべての型で動作します。型に関するすべての情報を含む Type オブジェクトを返します。ジェネリックを含め、任意の型を型引数として渡すことができます。 + +```typescript +import { typeOf } from '@deepkit/type'; + +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} + +typeOf<{id: number}>(); //{kind: 4, types: [{kind: 6, name: 'id'}]} + +class User { + id: number +} + +typeOf(); //{kind: 4, types: [...]} + +function test(id: number): string {} + +typeOf(); //{kind: 12, parameters: [...], return: {kind: 5}} +``` + +Type オブジェクトは、Type オブジェクトの型を示す `kind` プロパティを持つ単純なオブジェクトリテラルです。`kind` プロパティは数値で、enum `ReflectionKind` の値に対応します。`ReflectionKind` は `@deepkit/type` パッケージで次のように定義されています: + +```typescript +enum ReflectionKind { + never, //0 + any, //1 + unknown, //2 + void, //3 + object, //4 + string, //5 + number, //6 + boolean, //7 + symbol, //8 + bigint, //9 + null, //10 + undefined, //11 + + //... さらに多数 +} +``` + +返される可能性のある Type オブジェクトはいくつかあります。最も単純なものは `never`, `any`, `unknown`, `void, null,` および `undefined` で、次のように表現されます: + +```typescript +{kind: 0}; //never +{kind: 1}; //any +{kind: 2}; //unknown +{kind: 3}; //void +{kind: 10}; //null +{kind: 11}; //undefined +``` + +たとえば、数値 0 は `ReflectionKind` enum の最初のエントリ、つまり `never` を表し、数値 1 は 2 番目のエントリ、つまり `any` を表す、といった具合です。これに応じて、`string`、`number`、`boolean` のようなプリミティブ型は次のように表現されます: + +```typescript +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} +typeOf(); //{kind: 7} +``` + +これらの比較的単純な型は、`typeOf` に直接型引数として渡されているため、Type オブジェクトにはそれ以上の情報はありません。しかし、型エイリアス経由で型が渡される場合、Type オブジェクトには追加情報が含まれます。 + +```typescript +type Title = string; + +typeOf(); //{kind: 5, typeName: 'Title'} +``` + +この場合、型エイリアス名 'Title' も取得できます。型エイリアスがジェネリックの場合、渡された型も Type オブジェクトに保持されます。 + +```typescript +type Title<T> = T extends true ? string : number; + +typeOf<Title<true>>(); +{kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]} +``` + +渡された型がインデックスアクセス演算子の結果である場合、コンテナとインデックスタイプも保持されます: + +```typescript +interface User { + id: number; + username: string; +} + +typeOf<User['username']>(); +{kind: 5, indexAccessOrigin: { + container: {kind: Reflection.objectLiteral, types: [...]}, + Index: {kind: Reflection.literal, literal: 'username'} +}} +``` + +Interface とオブジェクトリテラルはどちらも Reflection.objectLiteral として出力され、`types` 配列に Property と Method を含みます。 + +```typescript +interface User { + id: number; + username: string; + login(password: string): void; +} + +typeOf<User>(); +{ + kind: Reflection.objectLiteral, + types: [ + {kind: Reflection.propertySignature, name: 'id', type: {kind: 6}}, + {kind: Reflection.propertySignature, name: 'username', + type: {kind: 5}}, + {kind: Reflection.methodSignature, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} + +type User = { + id: number; + username: string; + login(password: string): void; +} +typeOf<User>(); //上と同じオブジェクトを返します +``` + +インデックスシグネチャも `types` 配列に含まれます。 + +```typescript +interface BagOfNumbers { + [name: string]: number; +} + + +typeOf<BagOfNumbers>; +{ + kind: Reflection.objectLiteral, + types: [ + { + kind: Reflection.indexSignature, + index: {kind: 5}, //string + type: {kind: 6}, //number + } + ] +} + +type BagOfNumbers = { + [name: string]: number; +} +typeOf<BagOfNumbers>(); //上と同じオブジェクトを返します +``` + +Class はオブジェクトリテラルと似ており、`classType`(Class 自体への参照)に加えて、`types` 配列の下に Property と Method を持ちます。 + +```typescript +class User { + id: number = 0; + username: string = ''; + login(password: string): void { + //何もしない + } +} + +typeOf<User>(); +{ + kind: Reflection.class, + classType: User, + types: [ + {kind: Reflection.property, name: 'id', type: {kind: 6}}, + {kind: Reflection.property, name: 'username', + type: {kind: 5}}, + {kind: Reflection.method, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} +``` + +Reflection.propertySignature の種別が Reflection.property に、Reflection.methodSignature が Reflection.method に変わっていることに注意してください。Class 上の Property と Method には追加の属性があるため、この情報も取得できます。後者には `visibility`、`abstract`、`default` などが含まれます。 +Class の Type オブジェクトには、その Class 自身の Property と Method のみが含まれ、スーパークラスのものは含まれません。これは、インターフェイスやオブジェクトリテラルの Type オブジェクトとは対照的で、そちらではすべての親の PropertySignature と MethodSignature が解決されて `types` に含まれます。スーパークラスの Property と Method を解決するには、ReflectionClass とその `ReflectionClass.getProperties()`(後続のセクション参照)または `@deepkit/type` の `resolveTypeMembers()` を使用できます。 + +Type オブジェクトには非常に多くの種類があります。たとえば、literal、テンプレートリテラル、promise、enum、union、array、tuple などです。どれが存在し、どの情報が利用できるかを知るには、`@deepkit/type` から `Type` を import することをお勧めします。これは TypeAny、TypeUnknonwn、TypeVoid、TypeString、TypeNumber、TypeObjectLiteral、TypeArray、TypeClass など、すべての可能なサブタイプを含む `union` です。そこに正確な構造が記されています。 + +## Type キャッシュ + +ジェネリック引数が渡されない限り、型エイリアス、Function、Class の Type オブジェクトはキャッシュされます。つまり、`typeOf<MyClass>()` の呼び出しは常に同じオブジェクトを返します。 + +```typescript +type MyType = string; + +typeOf<MyType>() === typeOf<MyType>(); //true +``` + +しかし、ジェネリック型が使用されると、渡される型が常に同じであっても、常に新しいオブジェクトが作成されます。これは、理論上無限の組み合わせが可能であり、そのようなキャッシュは事実上メモリリークになるためです。 + +```typescript +type MyType<T> = T; + +typeOf<MyType<string>>() === typeOf<MyType<string>>(); +//false +``` + +ただし、再帰的な型の中で同じ型が複数回インスタンス化される場合、その間はキャッシュされます。ただし、そのキャッシュの存続期間は型が計算されている間に限られ、その後は存在しません。また、Type オブジェクト自体はキャッシュされますが、新しい参照が返され、全く同一のオブジェクトではありません。 + +```typescript +type MyType<T> = T; +type Object = { + a: MyType<string>; + b: MyType<string>; +}; + +typeOf<Object>(); +``` + +`Object` が計算されている間、`MyType<string>` はキャッシュされます。したがって `a` と `b` の PropertySignature はキャッシュから同じ `type` を持ちますが、同一の Type オブジェクトではありません。 + +ルート以外のすべての Type オブジェクトには parent プロパティがあり、通常は外側の親を指します。これは、たとえばある Type が union の一部かどうかを判断するのに役立ちます。 + +```typescript +type ID = string | number; + +typeOf<ID>(); +*Ref 1* { + kind: ReflectionKind.union, + types: [ + {kind: ReflectionKind.string, parent: *Ref 1* } } + {kind: ReflectionKind.number, parent: *Ref 1* } + ] +} +``` + +'Ref 1' は実際の union の Type オブジェクトを指します。 + +上記のようにキャッシュされた Type オブジェクトでは、`parent` プロパティが常に本来の親を指しているとは限りません。たとえば、ある Class が複数回使用される場合、`types` 内の直近の型(TypePropertySignature および TypeMethodSignature)は正しい TypeClass を指しますが、これらのシグネチャ型の `type` はキャッシュされたエントリの TypeClass のシグネチャ型を指します。親構造を無限に辿らず直近の親のみを読むために、これは重要な知識です。parent が無限の精度を持たないのは、パフォーマンス上の理由によるものです。 + +## JIT キャッシュ + +以降では、しばしば Type オブジェクトに基づく関数や機能について説明します。その一部を高性能に実装するため、Type オブジェクトごとの JIT(just in time)キャッシュが必要になります。これは `getJitContainer(type)` を介して提供できます。この関数は任意のデータを保存できる単純なオブジェクトを返します。そのオブジェクトへの参照が保持されない限り、Type オブジェクト自体が参照されなくなった時点で GC によって自動的に削除されます。 + +## Reflection クラス + +`typeOf<>()` 関数に加えて、Type オブジェクトに対する OOP 代替を提供するさまざまなリフレクション用クラスがあります。これらのリフレクション用クラスは Class、インターフェイス/オブジェクトリテラル、および Function とその直接のサブタイプ(Properties, Methods, Parameters)に対してのみ利用できます。より深い型は、再び Type オブジェクトで読み取る必要があります。 + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + + +const reflection = ReflectionClass.from<User>(); + +reflection.getProperties(); //[ReflectionProperty, ReflectionProperty] +reflection.getProperty('id'); //ReflectionProperty + +reflection.getProperty('id').name; //'id' +reflection.getProperty('id').type; //{kind: ReflectionKind.number} +reflection.getProperty('id').isOptional(); //false +``` + +## 型情報の受け取り + +型に対して動作する関数を提供するために、ユーザーに型を手動で渡してもらえるようにするのが有用な場合があります。たとえばバリデーション関数では、要求する型を最初の型引数として、検証対象のデータを最初の関数引数として渡せるようにすると便利です。 + +```typescript +validate<string>(1234); +``` + +この関数が `string` 型を取得できるようにするには、そのことを型コンパイラに伝える必要があります。 + +```typescript +function validate<T>(data: any, type?: ReceiveType<T>): void; +``` + +最初の型引数 `T` への参照を持つ `ReceiveType` は、`validate` の各呼び出しで(`type` が第2引数として宣言されているため)その型を2番目の位置に配置するよう、型コンパイラに指示します。実行時にその情報を読み出すには、`resolveReceiveType` 関数を使用します。 + +```typescript +import { resolveReceiveType, ReceiveType } from '@deepkit/type'; + +function validate<T>(data: any, type?: ReceiveType<T>): void { + type = resolveReceiveType(type); +} +``` + +不要に新しい変数を作らないよう、結果を同じ変数に代入するのが有用です。`type` には、型引数が渡されなかった、Deepkit の型コンパイラが正しくインストールされていない、もしくは型情報の出力が有効化されていない(上記のインストールのセクションを参照)といった場合に、Type オブジェクトが格納されるか、あるいは Error がスローされます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/serialization.md b/website/src/translations/ja/documentation/runtime-types/serialization.md new file mode 100644 index 000000000..32b801daf --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/serialization.md @@ -0,0 +1,153 @@ +# シリアライズ + +シリアライズは、例えばデータ型を転送や保存に適した形式へ変換するプロセスです。デシリアライズはその逆を行うプロセスです。これはロスレスで行われ、データ型情報やデータそのものを失うことなく、シリアライズの対象形式との間で相互に変換できます。 + +JavaScript におけるシリアライズは、通常 JavaScript のオブジェクトと JSON の間で行われます。JSON がサポートするのは String、Number、Boolean、Objects、Arrays のみです。一方で JavaScript は、BigInt、ArrayBuffer、型付き配列、Date、カスタムクラスのインスタンスなど、他にも多くの型をサポートします。さて、JSON を使って JavaScript のデータをサーバーに送信するには、シリアライズ処理(クライアント側)とデシリアライズ処理(サーバー側)が必要です。逆に、サーバーが JSON としてデータをクライアントに送る場合も同様です。これはロスレスではないため、`JSON.parse` と `JSON.stringify` だけでは不十分であることが多いです。 + +このシリアライズ処理は、非自明なデータにおいては絶対に必要です。というのも、JSON は日付のような基本型ですら情報を失ってしまうからです。新しい Date は最終的に JSON では文字列としてシリアライズされます: + +```typescript +const json = JSON.stringify(new Date); +//'"2022-05-13T20:48:51.025Z" +``` + +ご覧のとおり、JSON.stringify の結果は JSON の文字列です。これを JSON.parse で再びデシリアライズしても、Date オブジェクトではなく文字列になります。 + +```typescript +const value = JSON.parse('"2022-05-13T20:48:51.025Z"'); +//"2022-05-13T20:48:51.025Z" +``` + +JSON.parse に Date オブジェクトのデシリアライズを学習させるさまざまな回避策はありますが、エラーが起きやすく、性能もよくありません。このケースや他の多くの型に対して型安全なシリアライズ/デシリアライズを可能にするには、シリアライズ処理が必要です。 + +利用可能な主要な関数は 4 つあります: `serialize`、`cast`、`deserialize`、`validatedDeserialize`。これらの関数の内部では、デフォルトで `@deepkit/type` のグローバルに利用可能な JSON シリアライザが使用されますが、カスタムのシリアライズターゲットも使用できます。 + +Deepkit Type はユーザー定義のシリアライズターゲットをサポートしていますが、すでに強力な JSON シリアライズターゲットが付属しており、データを JSON オブジェクトとしてシリアライズし、その後 `JSON.stringify` を用いて正しく安全に JSON に変換できます。`@deepkit/bson` を使えば、BSON もシリアライズターゲットとして使用できます。カスタムのシリアライズターゲット(例えばデータベースドライバ向け)を作成する方法は、カスタムシリアライザのセクションで学べます。 + +なお、シリアライザも互換性のためにデータを検証しますが、これらの検証は [検証](validation.md) にある検証とは異なります。`cast` 関数だけは、デシリアライズが成功した後に [検証](validation.md) 章にある完全な検証プロセスも呼び出し、データが不正な場合はエラーをスローします。 + +代替として、デシリアライズ後の検証に `validatedDeserialize` を使用することもできます。別の方法としては、`deserialize` 関数でデシリアライズしたデータに対して `validate` または `validates` 関数を手動で呼び出すこともできます。詳しくは [検証](validation.md) を参照してください。 + +シリアライズと検証のすべての関数は、エラー時に `@deepkit/type` の `ValidationError` をスローします。 + +## キャスト + +`cast` 関数は、最初の型引数として TypeScript の型を、2 番目の引数としてキャストするデータを受け取ります。データは指定された型にキャストされ、成功すればそのデータが返されます。データが指定された型と互換性がなく自動的に変換できない場合は、`ValidationError` がスローされます。 + +```typescript +import { cast } from '@deepkit/type'; + +cast<string>(123); //'123' +cast<number>('123'); //123 +cast<number>('asdasd'); // ValidationError をスロー + +cast<string | number>(123); //123 +``` + +```typescript +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = cast<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); +``` + +`deserialize` 関数は `cast` に似ていますが、データが指定された型と互換性がない場合でもエラーをスローしません。代わりに、可能な限りデータを変換し、その結果を返します。データが指定された型と互換性がない場合は、そのまま返されます。 + +## シリアライズ + +```typescript +import { serialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const model = new MyModel('Peter'); + +const jsonObject = serialize<MyModel>(model); +//{ +// id: 0, +// created: '2021-06-10T15:07:24.292Z', +// name: 'Peter' +//} +const json = JSON.stringify(jsonObject); +``` + +`serialize` 関数は、渡されたデータをデフォルトで JSON シリアライザにより JSON オブジェクト、つまり String、Number、Boolean、Object、または Array に変換します。その結果は `JSON.stringify` を使って安全に JSON に変換できます。 + +## デシリアライズ + +`deserialize` 関数は、渡されたデータをデフォルトで JSON シリアライザにより、指定された型に対応するものへと変換します。JSON シリアライザは JSON オブジェクト、すなわち string、number、boolean、object、または array を想定します。これは通常 `JSON.parse` の呼び出しによって得られます。 + +```typescript +import { deserialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = deserialize<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); + +//JSON から +const json = '{"id": 5, "created": "Sat Oct 13 2018 14:17:35 GMT+0200", "name": "Peter"}'; +const myModel = deserialize<MyModel>(JSON.parse(json)); +``` + +正しいデータ型がすでに渡されている場合(例えば `created` には Date オブジェクト)、そのまま使用されます。 + +クラスだけでなく、任意の TypeScript の型を最初の型引数として指定できます。したがって、プリミティブや非常に複雑な型でも渡せます: + +```typescript +deserialize<Date>('Sat Oct 13 2018 14:17:35 GMT+0200'); +deserialize<string | number>(23); +``` + +<a name="loosely-convertion"></a> +### 緩やかな型変換 + +デシリアライズの過程で緩やかな型変換が実装されています。これは、String 型に対して String や Number を、String 型に対して Number を受け入れて自動的に変換できることを意味します。例えば、URL 経由でデータを受け取りデシリアライザに渡す場合に便利です。URL は常に文字列なので、Deepkit Type は Number と Boolean の型解決も試みます。 + +```typescript +deserialize<boolean>('false')); //false +deserialize<boolean>('0')); //false +deserialize<boolean>('1')); //true + +deserialize<number>('1')); //1 + +deserialize<string>(1)); //'1' +``` + +JSON シリアライザには、次の緩やかな型変換が組み込まれています: + +* *number|bigint*: Number あるいは Bigint は String、Number、BigInt を受け入れます。変換が必要な場合は `parseFloat` または `BigInt(x)` が使われます。 +* *boolean*: Boolean は Number と String を受け入れます。0、'0'、'false' は `false` と解釈されます。1、'1'、'true' は `true` と解釈されます。 +* *string*: String は Number、String、Boolean など多くを受け入れます。文字列以外の値は自動的に `String(x)` で変換されます。 + +緩やかな変換は無効化することもできます: + +```typescript +const result = deserialize(data, {loosely: false}); +``` + +無効なデータの場合、変換は試みられず、代わりにエラーがスローされます。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/types.md b/website/src/translations/ja/documentation/runtime-types/types.md new file mode 100644 index 000000000..039a2a9a0 --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/types.md @@ -0,0 +1,493 @@ +# 型アノテーション + +型アノテーションは、実行時に読み取られ、さまざまな関数の挙動を変更できるメタ情報を含む通常の TypeScript の Type です。Deepkit には、すでに多くのユースケースをカバーする型アノテーションが用意されています。例えば、Class の Property を主キー、参照、インデックスとしてマークできます。データベースライブラリは、事前のコード生成なしに、この情報を実行時に使用して正しい SQL クエリを作成できます。 + +`MaxLength`、`Maximum`、`Positive` といったバリデータ制約も任意の型に追加可能です。特定の値をどのようにシリアライズ/デシリアライズするかをシリアライザに指定することもできます。さらに、完全にカスタムな型アノテーションを作成し、それらを実行時に読み取ることで、型システムを非常に個別的な方法で実行時に利用することも可能です。 + +Deepkit には一連の型アノテーションが付属しており、すべて `@deepkit/type` から直接使用できます。これらは複数のライブラリからではなく、Deepkit RPC や Deepkit Database といった特定のライブラリにコードを直接結び付けないように設計されています。これにより、たとえデータベース型アノテーションを使用していても、フロントエンドでの型の再利用が容易になります。 + +以下は既存の型アノテーションの一覧です。`@deepkit/type` と `@deepkit/bson` のバリデータとシリアライザ、そして `@deepkit/orm` の Deepkit Database は、この情報をそれぞれ異なる方法で使用します。詳細は対応する章を参照してください。 + +## Integer/Float + +整数と浮動小数点は、基底として `number` で定義され、いくつかのサブバリアントがあります: + +| 型 | 説明 | +|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| integer | 任意サイズの整数。 | +| int8 | -128 から 127 までの整数。 | +| uint8 | 0 から 255 までの整数。 | +| int16 | -32768 から 32767 までの整数。 | +| uint16 | 0 から 65535 までの整数。 | +| int32 | -2147483648 から 2147483647 までの整数。 | +| uint32 | 0 から 4294967295 までの整数。 | +| float | number と同じですが、データベースの文脈では異なる意味を持つ場合があります。 | +| float32 | -3.40282347e+38 から 3.40282347e+38 の間の float。JavaScript は精度の問題により範囲を正しく検証できないことに注意してください。ただし、この情報はデータベースやバイナリシリアライザには有用です。 | +| float64 | number と同じですが、データベースの文脈では異なる意味を持つ場合があります。 | + +```typescript +import { integer } from '@deepkit/type'; + +interface User { + id: integer; +} +``` + +ここで、ユーザーの `id` は実行時には number ですが、バリデーションやシリアライゼーションでは整数として解釈されます。 +つまり、例えばバリデーションでは float は使用できず、シリアライザは自動的に float を整数に変換します。 + +```typescript +import { is, integer } from '@deepkit/type'; + +is<integer>(12); //true +is<integer>(12.5); //false +``` + +サブタイプも同様に使用でき、許可する数値範囲を特定したい場合に有用です。 + +```typescript +import { is, int8 } from '@deepkit/type'; + +is<int8>(-5); //true +is<int8>(5); //true +is<int8>(-200); //false +is<int8>(2500); //false +``` + +```typescript +import { is, float, float32, float64 } from '@deepkit/type'; +is<float>(12.5); //true +is<float32>(12.5); //true +is<float64>(12.5); //true +```` + +## UUID + +UUID v4 は通常、データベースではバイナリとして、JSON では文字列として保存されます。 + +```typescript +import { is, UUID } from '@deepkit/type'; + +is<UUID>('f897399a-9f23-49ac-827d-c16f8e4810a0'); //true +is<UUID>('asd'); //false +``` + +## MongoID + +このフィールドを MongoDB の ObjectId としてマークします。解決結果は文字列です。MongoDB ではバイナリとして保存されます。 + +```typescript +import { MongoId, serialize, is } from '@deepkit/type'; + +serialize<MongoId>('507f1f77bcf86cd799439011'); //507f1f77bcf86cd799439011 +is<MongoId>('507f1f77bcf86cd799439011'); //true +is<MongoId>('507f1f77bcf86cd799439011'); //false + +class User { + id: MongoId = ''; //ユーザーが挿入されると Deepkit ORM によって自動的に設定されます +} +``` + +## Bigint + +デフォルトでは通常の bigint 型は JSON では number(BSON では long)としてシリアライズされます。しかし、JavaScript の bigint は潜在的に無制限のサイズを持つのに対し、JavaScript の number や BSON の long は制限があるため、保存できる内容には制約があります。この制限を回避するために `BinaryBigInt` と `SignedBinaryBigInt` が利用可能です。 + +`BinaryBigInt` は bigint と同じですが、データベースでは無制限サイズの符号なしバイナリ(多くのデータベースの 8 バイトではなく)として、JSON では文字列としてシリアライズされます。負の値は正の値に変換されます(`abs(x)`)。 + +```typescript +import { BinaryBigInt } from '@deepkit/type'; + +interface User { + id: BinaryBigInt; +} + +const user: User = { id: 24n }; + +serialize<User>({ id: 24n }); //{id: '24'} + +serialize<BinaryBigInt>(24); //'24' +serialize<BinaryBigInt>(-24); //'0' +``` + +Deepkit ORM は BinaryBigInt をバイナリフィールドとして保存します。 + +`SignedBinaryBigInt` は `BinaryBigInt` と同様ですが、負の値も保存できます。Deepkit ORM は `SignedBinaryBigInt` をバイナリとして保存します。このバイナリには先頭に符号バイトが追加され、符号なし整数(uint)として表現されます: 負は 255、ゼロは 0、正は 1。 + +```typescript +import { SignedBinaryBigInt } from '@deepkit/type'; + +interface User { + id: SignedBinaryBigInt; +} +``` + +## MapName + +シリアライズ時に Property 名を変更します。 + +```typescript +import { serialize, deserialize, MapName } from '@deepkit/type'; + +interface User { + firstName: string & MapName<'first_name'>; +} + +serialize<User>({ firstName: 'Peter' }) // {first_name: 'Peter'} +deserialize<User>({ first_name: 'Peter' }) // {firstName: 'Peter'} +``` + +## Group + +Property をグループ化できます。シリアライズでは、例えば特定のグループを除外できます。詳細はシリアライゼーションの章を参照してください。 + +```typescript +import { serialize } from '@deepkit/type'; + +interface Model { + username: string; + password: string & Group<'secret'> +} + +serialize<Model>( + { username: 'Peter', password: 'nope' }, + { groupsExclude: ['secret'] } +); //{username: 'Peter'} +``` + +## Data + +各 Property は、リフレクション API 経由で読み取れる追加のメタデータを追加できます。詳細は[ランタイム型のリフレクション](runtime-types.md#runtime-types-reflection)を参照してください。 + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface Model { + username: string; + title: string & Data<'key', 'value'> +} + +const reflection = ReflectionClass.from<Model>(); +reflection.getProperty('title').getData()['key']; //value; +``` + +## Excluded + +各 Property は、特定のターゲットに対するシリアライズ処理から除外できます。 + +```typescript +import { serialize, deserialize, Excluded } from '@deepkit/type'; + +interface Auth { + title: string; + password: string & Excluded<'json'> +} + +const item = deserialize<Auth>({ title: 'Peter', password: 'secret' }); + +item.password; //undefined。deserialize のデフォルトのシリアライザは `json` であるため + +item.password = 'secret'; + +const json = serialize<Auth>(item); +json.password; //再び undefined。serialize のシリアライザは `json` のため +``` + +## Embedded + +フィールドを埋め込み型としてマークします。 + +```typescript +import { PrimaryKey, Embedded, serialize, deserialize } from '@deepkit/type'; + +interface Address { + street: string; + postalCode: string; + city: string; + country: string; +} + +interface User { + id: number & PrimaryKey; + address: Embedded<Address>; +} + +const user: User +{ + id: 12, + address +: + { + street: 'abc', postalCode + : + '1234', city + : + 'Hamburg', country + : + 'Germany' + } +} +; + +serialize<User>(user); +{ + id: 12, + address_street +: + 'abc', + address_postalCode +: + '1234', + address_city +: + 'Hamburg', + address_country +: + 'Germany' +} + +//deserialize では埋め込み構造を提供する必要があります +deserialize<User>({ + id: 12, + address_street: 'abc', + //... +}); +``` + +プレフィックス(デフォルトでは Property 名)を変更することも可能です。 + +```typescript +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: 'addr_' }>; +} + +serialize<User>(user); +{ + id: 12, + addr_street +: + 'abc', + addr_postalCode +: + '1234', +} + +//または完全に削除する +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: '' }>; +} + +serialize<User>(user); +{ + id: 12, + street +: + 'abc', + postalCode +: + '1234', +} +``` + +## Entity + +Interface にエンティティ情報を注釈します。データベースの文脈でのみ使用されます。 + +```typescript +import { Entity, PrimaryKey } from '@deepkit/type'; + +interface User extends Entity<{ name: 'user', collection: 'users'> { + id: number & PrimaryKey; + username: string; +} +``` + +## PrimaryKey + +フィールドを主キーとしてマークします。データベースの文脈でのみ使用されます。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## AutoIncrement + +フィールドをオートインクリメントとしてマークします。データベースの文脈でのみ使用されます。 +通常は `PrimaryKey` と併用します。 + +```typescript +import { AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## Reference + +フィールドを参照(外部キー)としてマークします。データベースの文脈でのみ使用されます。 + +```typescript +import { Reference } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; +} +``` + +この例では `User.group` は所有側の参照で、SQL における外部キーとしても知られています。つまり、`User` テーブルには `group` カラムがあり、`Group` テーブルを参照しています。`Group` テーブルはこの参照のターゲットテーブルです。 + +## BackReference + +フィールドを逆参照としてマークします。データベースの文脈でのみ使用されます。 + +```typescript + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; + users: User[] & BackReference; +} +``` + +この例では `Group.users` は逆参照です。これは、`User` テーブルに `group` カラムがあり、それが `Group` テーブルを参照していることを意味します。 +`Group` は仮想の Property `users` を持ち、結合を伴うデータベースクエリが実行されると、`Group` の id と同じ `group` id を持つすべてのユーザーで自動的に埋められます。Property `users` はデータベースには保存されません。 + +## Index + +フィールドをインデックスとしてマークします。データベースの文脈でのみ使用されます。 + +```typescript +import { Index } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Index; +} +``` + +## Unique + +フィールドをユニークとしてマークします。データベースの文脈でのみ使用されます。 + +```typescript +import { Unique } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Unique; +} +``` + +## DatabaseField + +`DatabaseField` を使うと、実際のデータベースのカラム型やデフォルト値など、データベース固有のオプションを定義できます。 + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & DatabaseField<{ type: 'varchar(255)' }>; +} +``` + +## Validation + +TODO + +[バリデーション制約の型](validation.md#validation-constraint-types)を参照してください。 + +## InlineRuntimeType + +ランタイム型をインライン化します。高度なケースでのみ使用されます。 + +```typescript +import { InlineRuntimeType, ReflectionKind, Type } from '@deepkit/type'; + +const type: Type = { kind: ReflectionKind.string }; + +type Query = { + field: InlineRuntimeType<typeof type>; +} + +const resolved = typeOf<Query>(); // { field: string } +``` + +TypeScript では型 `Query` は `{ field: any }` ですが、実行時には `{ field: string }` です。 + +これは、ランタイム型を受け取り、さまざまな場面で再利用するような高いカスタマイズ性を持つシステムを構築する場合に有用です。 + +## ResetAnnotation + +Property のすべてのアノテーションをリセットします。高度なケースでのみ使用されます。 + +```typescript +import { ResetAnnotation } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} + +interface UserCreationPayload { + id: User['id'] & ResetAnnotation<'primaryKey'>; +} +``` + +### カスタム型アノテーション + +独自の型アノテーションを定義できます。 + +```typescript +type MyAnnotation = { __meta?: ['myAnnotation'] }; +``` + +慣例として、型アノテーションは単一の省略可能な Property `__meta` を持つオブジェクトリテラルとして定義され、その型はタプルです。このタプルの最初の要素が一意の名前で、以降の要素は任意のオプションです。これにより、型アノテーションに追加のオプションを装備できます。 + +```typescript +type AnnotationOption<T extends { title: string }> = { __meta?: ['myAnnotation', T] }; +``` + +型アノテーションは交差型演算子 `&` と共に使用します。1 つの型に任意の数の型アノテーションを使用できます。 + +```typescript +type Username = string & MyAnnotation; +type Title = string & MyAnnotation & AnnotationOption<{ title: 'Hello' }>; +``` + +型アノテーションは、`typeOf<T>()` と `typeAnnotation` の型オブジェクトを介して読み取れます: + +```typescript +import { typeOf, typeAnnotation } from '@deepkit/type'; + +const type = typeOf<Username>(); +const annotation = typeAnnotation.getForName(type, 'myAnnotation'); //[] +``` + +`annotation` の結果は、型アノテーション `myAnnotation` が使用されていればオプションを持つ配列、使用されていなければ `undefined` です。`AnnotationOption` に見られるように、型アノテーションに追加オプションがある場合、渡された値はその配列の中に見つかります。 +すでに提供されている `MapName`、`Group`、`Data` などの型アノテーションには、それぞれ専用のアノテーションオブジェクトがあります: + +```typescript +import { typeOf, Group, groupAnnotation } from '@deepkit/type'; + +type Username = string & Group<'a'> & Group<'b'>; + +const type = typeOf<Username>(); +groupAnnotation.getAnnotations(type); //['a', 'b'] +``` + +詳しくは[ランタイム型のリフレクション](./reflection.md)を参照してください。 \ No newline at end of file diff --git a/website/src/translations/ja/documentation/runtime-types/validation.md b/website/src/translations/ja/documentation/runtime-types/validation.md new file mode 100644 index 000000000..3f3a7be9a --- /dev/null +++ b/website/src/translations/ja/documentation/runtime-types/validation.md @@ -0,0 +1,549 @@ +# バリデーション + +バリデーションは、データの正確性と整合性を検証する体系的なプロセスです。これは、データ型が期待される型に一致しているかどうかだけでなく、追加で定義された制約が満たされているかどうかの確認も含みます。 + +不確実または信頼できないソースからのデータを扱う際には、バリデーションが最重要となります。「不確実」なソースとは、データの型や内容が予測できず、実行時に任意の値を取りうるものを指します。典型例には、ユーザー入力、HTTP リクエスト(クエリパラメータやボディなど)、CLI 引数、プログラムに読み込まれるファイルなどがあります。このようなデータは本質的にリスクがあり、誤った型や値はプログラムの障害やセキュリティ脆弱性の原因になりえます。 + +例えば、Variable に数値を格納することが期待される場合、実際に数値が入っていることを検証することは極めて重要です。不一致は予期せぬクラッシュやセキュリティ侵害につながります。 + +たとえば HTTP ルートコントローラを設計する際には、クエリパラメータやリクエストボディなど、あらゆるユーザー入力のバリデーションを最優先にすべきです。特に TypeScript を使用する環境では、型キャストの使用を避けることが重要です。型キャストは誤解を招き、根本的なセキュリティリスクをもたらします。 + +```typescript +app.post('/user', function(request) { + const limit = request.body.limit as number; +}); +``` + +コーディングで頻繁に見られる Error は、実行時の安全性を提供しない型キャストに関するものです。例えば、Variable を number に型キャストしても、ユーザーが string を入力した場合、プログラムは string を number として扱ってしまいます。このような見落としはシステムクラッシュや深刻なセキュリティ上の脅威を引き起こす可能性があります。これらのリスクを軽減するため、開発者はバリデータや型ガードを活用できます。さらに、シリアライザを用いて Variable を変換(たとえば 'limit' を number に変換)することも役立ちます。このトピックの詳細はシリアライゼーションの章で確認できます。 + +バリデーションは単なる選択肢ではなく、堅牢なソフトウェア設計の不可欠な構成要素です。常に用心深くあるべきで、過剰なくらいにバリデーションする方が、後で不十分だったと後悔するよりも良いのです。Deepkit はこの重要性を理解しており、豊富なバリデーションツールを提供します。さらに、高性能な設計により、実行時間への影響は最小限に抑えられています。指針として、たとえ冗長に感じられる場合でも、アプリケーションを保護するために包括的なバリデーションを実施してください。 + +Deepkit の多くのコンポーネント(HTTP ルーター、RPC 抽象化、データベース抽象化を含む)には、組み込みのバリデーションシステムがあります。これらは自動的にトリガーされ、多くの場合、手動での介入は不要です。 + + +自動バリデーションがいつ、どのように行われるかの包括的な理解については、各章([CLI](../cli.md)、[HTTP](../http.md)、[RPC](../rpc.md)、[ORM](../orm.md))を参照してください。 +必要な制約やデータ型に慣れておきましょう。適切に定義された Parameter により、Deepkit の自動バリデーション機能を引き出し、手作業を減らし、よりクリーンで安全なコードを実現できます。 + +## 使い方 + +バリデータの基本的な Function は、値の型をチェックすることです。例えば、値が string かどうかなど。ここで扱うのは文字列の内容ではなく、その型のみです。TypeScript には多くの型があり、string、number、boolean、bigint、objects、classes、interface、generics、mapped types など多岐にわたります。TypeScript の強力な型システムにより、非常に多様な型が利用可能です。 + +JavaScript 自体では、プリミティブ型は `typeof` 演算子で判定できます。interface、mapped types、または汎用的な set/map のような複雑な型では、もはや容易ではないため、`@deepkit/type` のようなバリデータライブラリが必要になります。Deepkit は、TypeScript のあらゆる型を回避策なしで直接バリデーションできる唯一のソリューションです。 + + + +Deepkit では、型のバリデーションは `validate`、`is`、`assert` のいずれかの Function を使用して行えます。 +`is` はいわゆる型ガードであり、`assert` は型アサーションです。両者については次のセクションで説明します。 +`validate` は見つかったエラーの配列を返し、成功時は空配列を返します。この配列の各エントリは、正確なエラーコードとエラーメッセージ、さらに objects や arrays のような複雑な型がバリデーションされた場合のパスを説明します。 + +これら 3 つの Function はほぼ同じ方法で使用します。型は最初の型引数で指定または参照し、データは最初の引数として渡します。 + +```typescript +import { validate, is, assert } from '@deepkit/type'; + +const errors = validate<string>('abc'); //[] +const errors = validate<string>(123); //[{code: 'type', message: 'string ではありません'}] + +if (is<string>(value)) { + // value は string であることが保証されます +} + +function doSomething(value: any) { + assert<string>(value); //無効なデータの場合は例外をスローします + + // value は string であることが保証されます +} +``` + +classes や interface のような、より複雑な型を扱う場合、配列には複数のエントリが含まれることがあります。 + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>(undefined); //[{code: 'type', message: 'オブジェクトではありません'}] + +validate<User>({}); +//[ +// {path: 'id', code: 'type', message: 'number ではありません'}], +// {path: 'username', code: 'type', message: 'string ではありません'}], +//] +``` + +バリデータは深い再帰的な型もサポートします。パスはドットで区切られます。 + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; + supervisor?: User; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>({id: 1, username: 'Joe', supervisor: {}}); +//[ +// {path: 'supervisor.id', code: 'type', message: 'number ではありません'}], +// {path: 'supervisor.username', code: 'type', message: 'string ではありません'}], +//] +``` + +TypeScript の利点を活用しましょう。例えば、`User` のような複雑な型は、何度も宣言し直すことなく複数箇所で再利用できます。`User` を `id` なしでバリデートしたい場合、TypeScript のユーティリティ型を使って素早く効率的に派生サブタイプを作成できます。まさに DRY (Don't Repeat Yourself) の精神です。 + +```typescript +type UserWithoutId = Omit<User, 'id'>; + +validate<UserWithoutId>({username: 'Joe'}); //有効です! +``` + +Deepkit は、実行時にこのような形で TypeScript の型へアクセスできる唯一の主要フレームワークです。フロントエンドとバックエンドの両方で型を使いたい場合は、型を別ファイルに切り出してどこからでも import できます。これを活用して、コードを効率的かつクリーンに保ちましょう。 + +## 型キャストは安全ではない + +TypeScript における型キャスト(型ガードとは対照的)は実行時の構造ではなく、型システム内でのみ扱われます。未知のデータに型を割り当てる安全な方法ではありません。 + +```typescript +const data: any = ...; + +const username = data.username as string; + +if (username.startsWith('@')) { //クラッシュする可能性があります +} +``` + +`as string` のコードは安全ではありません。Variable `data` は文字通りあらゆる値(例えば `{username: 123}` や `{}`)を取りうるため、その結果 `username` が string ではなく全く別のものであり、したがって `username.startsWith('@')` のコードはエラーになり、軽微なケースではプログラムがクラッシュし、最悪の場合はセキュリティ脆弱性が生まれます。 +ここで実行時に `data` が string 型の `username` Property を持つことを保証するには、型ガードを使用する必要があります。 + +型ガードは、渡されたデータが実行時に保証される型について TypeScript にヒントを与える Function です。この知識を元に、TypeScript はコードの進行に応じて型を「絞り込み」ます。例えば、`any` を安全に string にしたり、他の型にしたりできます。したがって、型が不明(`any` や `unknown`)なデータがある場合、型ガードはデータ自体に基づいてより正確に絞り込むのに役立ちます。ただし、型ガードの安全性はその実装に依存します。もし誤りがあれば、根本的な前提が突然真実でないことが明らかになり、重大な結果を招く可能性があります。 + +<a name="type-guard"></a> + +## 型ガード + +上で使用した `User` 型に対する型ガードは、最も単純な形では次のようになります。なお、前述した NaN の特殊性は考慮されていないため、この型ガードは完全には正しくありません。 + +```typescript +function isUser(data: any): data is User { + return 'object' === typeof data + && 'number' === typeof data.id + && 'string' === typeof data.username; +} + +isUser({}); //false + +isUser({id: 1, username: 'Joe'}); //true +``` + +型ガードは常に boolean を返し、通常は if 文で直接使用されます。 + +```typescript +const data: any = await fetch('/user/1'); + +if (isUser(data)) { + data.id; //安全にアクセスでき、number です +} +``` + +特に複雑な型に対して、各型ガードごとに個別の Function を書き、型が変わるたびにそれを調整するのは非常に面倒で、エラーを誘発し、非効率です。そこで Deepkit は、任意の TypeScript 型に対する型ガードを自動的に提供する `is` Function を用意しています。これは NaN の問題のような特殊性も自動的に考慮します。`is` は `validate` と同じことを行いますが、エラー配列の代わりに boolean を返します。 + +```typescript +import { is } from '@deepkit/type'; + +is<string>('abc'); //true +is<string>(123); //false + + +const data: any = await fetch('/user/1'); + +if (is<User>(data)) { + //data は今や User 型であることが保証されています +} +``` + +よく見られるパターンとして、バリデーション失敗時に即座に Error を返して後続のコードを実行しないようにする方法があります。これはコード全体のフローを変更せずに、さまざまな場所で使用できます。 + +```typescript +function addUser(data: any): void { + if (!is<User>(data)) throw new TypeError('No user given'); + + //data は今や User 型であることが保証されています +} +``` + +あるいは、TypeScript の型アサーションを使用する方法もあります。`assert` Function は、与えられたデータが型に正しくバリデートされない場合、自動的に Error をスローします。TypeScript の型アサーションを特徴づける特別なシグネチャにより、TypeScript は渡された Variable を自動的に絞り込みます。 + +```typescript +import { assert } from '@deepkit/type'; + +function addUser(data: any): void { + assert<User>(data); //無効なデータなら例外をスローします + + //data は今や User 型であることが保証されています +} +``` + +ここでも、TypeScript が提供する利点を活用してください。型はさまざまな TypeScript の機能を使って再利用やカスタマイズが可能です。 + +<a name="error-reporting"></a> + +## エラー報告 + +`is`、`assert`、`validates` は結果として boolean を返します。失敗したバリデーションルールに関する正確な情報を得るには、`validate` Function を使用します。すべてが正常にバリデートされると空配列を返します。エラーがある場合、配列には次の構造を持つ 1 件以上のエントリが含まれます。 + +```typescript +interface ValidationErrorItem { + /** + * Property へのパス。ドットで区切られた深いパスの場合があります。 + */ + path: string; + /** + * このエラーを識別・翻訳するために使用できる小文字のエラーコード。 + */ + code: string, + /** + * エラーの自由テキスト。 + */ + message: string, +} +``` + +この Function は、最初の型引数に任意の TypeScript 型を、最初の引数にバリデート対象のデータを受け取ります。 + +```typescript +import { validate } from '@deepkit/type'; + +validate<string>('Hello'); //[] +validate<string>(123); //[{code: 'type', message: 'string ではありません', path: ''}] + +validate<number>(123); //[] +validate<number>('Hello'); //[{code: 'type', message: 'number ではありません', path: ''}] +``` + +interface、classes、generics などの複雑な型も使用できます。 + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>(undefined); //[{code: 'type', message: 'オブジェクトではありません', path: ''}] +validate<User>({}); //[{code: 'type', message: 'number ではありません', path: 'id'}] +validate<User>({id: 1}); //[{code: 'type', message: 'string ではありません', path: 'username'}] +validate<User>({id: 1, username: 'Peter'}); //[] +``` + +<a name="constraints"></a> + +## 制約 + +型のチェックに加えて、任意の制約を型に追加できます。これらの追加の内容制約のバリデーションは、型自体のバリデーション後に自動的に行われます。これは `validate`、`is`、`assert` といったすべてのバリデーション Function で行われます。 +例えば、string が特定の最小長または最大長を持つべきだという制約があります。これらの制約は[型アノテーション](./types.md)を通じて実際の型に追加されます。使用できるアノテーションは多種多様にあります。拡張が必要な場合は独自のアノテーションを自由に定義して使用できます。 + +```typescript +import { MinLength } from '@deepkit/type'; + +type Username = string & MinLength<3>; +``` + +`&` を使って、任意の数の型アノテーションを実際の型に追加できます。ここでの結果(`username`)は、すべてのバリデーション Function や他の型でも使用できます。 + +```typescript +import { is } from '@deepkit/type'; + +is<Username>('ab'); //false, 最小長が 3 のため +is<Username>('Joe'); //true + +interface User { + id: number; + username: Username; +} + +is<User>({id: 1, username: 'ab'}); //false, 最小長が 3 のため +is<User>({id: 1, username: 'Joe'}); //true +``` + +`validate` は制約に起因する有用なエラーメッセージを返します。 + +```typescript +import { validate } from '@deepkit/type'; + +const errors = validate<Username>('xb'); +//[{ code: 'minLength', message: `最小長は 3 です` }] +``` + +この情報は、フォームなどに自動的に表示するのに最適で、`code` を用いて翻訳することもできます。objects や arrays に対する既存のパスにより、フォーム内のフィールドは該当するエラーを抽出して表示できます。 + +```typescript +validate<User>({id: 1, username: 'ab'}); +//{ path: 'username', code: 'minLength', message: `最小長は 3 です` } +``` + +よくある有用なユースケースとして、RegExp 制約で email を定義することがあります。一度型を定義すれば、どこでも使用できます。 + +```typescript +export const emailRegexp = /^\S+@\S+$/; +type Email = string & Pattern<typeof emailRegexp> + +is<Email>('abc'); //false +is<Email>('joe@example.com'); //true +``` + +制約はいくつでも追加できます。 + +```typescript +type ID = number & Positive & Maximum<1000>; + +is<ID>(-1); //false +is<ID>(123); //true +is<ID>(1001); //true +``` + +### 制約の種類 + +#### Validate<typeof myValidator> + +カスタムバリデータ Function を使ったバリデーション。詳細は次節「カスタムバリデータ」を参照してください。 + +```typescript +import { ValidatorError, Validate } from '@deepkit/type'; + +function startsWith(v: string) { + return (value: any) => { + const valid = 'string' === typeof value && value.startsWith(v); + return valid ? undefined : new ValidatorError('startsWith', `「${v}」で始まりません`); + }; +} + +type T = string & Validate<typeof startsWith, 'abc'>; +``` + +#### Pattern<typeof myRegexp> + +正規表現をバリデーションパターンとして定義します。通常、E-Mail の検証やより複雑な内容の検証に使用します。 + +```typescript +import { Pattern } from '@deepkit/type'; + +const myRegExp = /[a-zA-Z]+/; +type T = string & Pattern<typeof myRegExp> +``` + +#### Alpha + +英字(a-Z)のバリデーション。 + +```typescript +import { Alpha } from '@deepkit/type'; + +type T = string & Alpha; +``` + + +#### Alphanumeric + +英数字のバリデーション。 + +```typescript +import { Alphanumeric } from '@deepkit/type'; + +type T = string & Alphanumeric; +``` + + +#### Ascii + +ASCII 文字のバリデーション。 + +```typescript +import { Ascii } from '@deepkit/type'; + +type T = string & Ascii; +``` + + +#### Decimal<number, number> + +0.1、.3、1.1、1.00003、4.0 など、10 進数を表す文字列のバリデーション。 + +```typescript +import { Decimal } from '@deepkit/type'; + +type T = string & Decimal<1, 2>; +``` + + +#### MultipleOf<number> + +指定した数値の倍数である number のバリデーション。 + +```typescript +import { MultipleOf } from '@deepkit/type'; + +type T = number & MultipleOf<3>; +``` + + +#### MinLength<number>, MaxLength<number>, MinMax<number, number> + +arrays または strings の最小/最大長のバリデーション。 + +```typescript +import { MinLength, MaxLength, MinMax } from '@deepkit/type'; + +type T = any[] & MinLength<1>; + +type T = string & MinLength<3> & MaxLength<16>; + +type T = string & MinMax<3, 16>; +``` + +#### Includes<'any'> Excludes<'any'> + +配列要素または部分文字列が含まれている/含まれていないことのバリデーション + +```typescript +import { Includes, Excludes } from '@deepkit/type'; + +type T = any[] & Includes<'abc'>; +type T = string & Excludes<' '>; +``` + +#### Minimum<number>, Maximum<number> + +与えられた数値以上/以下であることのバリデーション。`>=` および `<=` と同じです。 + +```typescript +import { Minimum, Maximum, MinMax } from '@deepkit/type'; + +type T = number & Minimum<10>; +type T = number & Minimum<10> & Maximum<1000>; + +type T = number & MinMax<10, 1000>; +``` + +#### ExclusiveMinimum<number>, ExclusiveMaximum<number> + +minimum/maximum と同じですが、値そのものを除外します。`>` および `<` と同じです。 + +```typescript +import { ExclusiveMinimum, ExclusiveMaximum } from '@deepkit/type'; + +type T = number & ExclusiveMinimum<10>; +type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>; +``` + + +#### Positive, Negative, PositiveNoZero, NegativeNoZero + +正または負であることのバリデーション。 + +```typescript +import { Positive, Negative } from '@deepkit/type'; + +type T = number & Positive; +type T = number & Negative; +``` + + +#### BeforeNow, AfterNow + +現在(new Date)と比較した日付値のバリデーション。 + +```typescript +import { BeforeNow, AfterNow } from '@deepkit/type'; + +type T = Date & BeforeNow; +type T = Date & AfterNow; +``` + +#### Email + +`/^\S+@\S+$/` による簡易な email の正規表現バリデーション。自動的に `string` なので、`string & Email` とする必要はありません。 + +```typescript +import { Email } from '@deepkit/type'; + +type T = Email; +``` + +#### integer + +指定された範囲の整数であることを保証します。自動的に `number` なので、`number & integer` とする必要はありません。 + +```typescript +import { integer, uint8, uint16, uint32, + int8, int16, int32 } from '@deepkit/type'; + +type T = integer; +type T = uint8; +type T = uint16; +type T = uint32; +type T = int8; +type T = int16; +type T = int32; +``` + +詳細は「Special types: integer/floats」を参照してください。 + +### カスタムバリデータ + +組み込みのバリデータで不足する場合は、`Validate` デコレータを介してカスタムバリデーション Function を作成して使用できます。 + +```typescript +import { ValidatorError, Validate, Type, validates, validate } + from '@deepkit/type'; + +function titleValidation(value: string, type: Type) { + value = value.trim(); + if (value.length < 5) { + return new ValidatorError('tooShort', '値が短すぎます'); + } +} + +interface Article { + id: number; + title: string & Validate<typeof titleValidation>; +} + +console.log(validates<Article>({id: 1})); //false +console.log(validates<Article>({id: 1, title: 'Peter'})); //true +console.log(validates<Article>({id: 1, title: ' Pe '})); //false +console.log(validate<Article>({id: 1, title: ' Pe '})); //[ValidationErrorItem] +``` + +カスタムバリデーション Function は、すべての組み込み型バリデータが呼び出された後に実行されることに注意してください。あるバリデータが失敗した場合、現在の型に対する後続のバリデータはすべてスキップされます。各型につき失敗は 1 回のみです。 + +#### ジェネリックバリデータ + +Validator Function では type オブジェクトが利用可能で、これを使ってバリデータが適用されている型に関するより詳しい情報を取得できます。また、`validate` 型に渡す任意のバリデータオプションを定義し、バリデータを設定可能にすることもできます。これらの情報と親参照により、強力なジェネリックバリデータを作成できます。 + +```typescript +import { ValidatorError, Validate, Type, is, validate } + from '@deepkit/type'; + +function startsWith(value: any, type: Type, chars: string) { + const valid = 'string' === typeof value && value.startsWith(chars); + if (!valid) { + return new ValidatorError('startsWith', '「' + chars + '」で始まりません') + } +} + +type MyType = string & Validate<typeof startsWith, 'a'>; + +is<MyType>('aah'); //true +is<MyType>('nope'); //false + +const errors = validate<MyType>('nope'); +//[{ path: '', code: 'startsWith', message: `「a」で始まりません` }]); +``` \ No newline at end of file diff --git a/website/src/translations/ja/state.json b/website/src/translations/ja/state.json new file mode 100644 index 000000000..73e29dfb6 --- /dev/null +++ b/website/src/translations/ja/state.json @@ -0,0 +1,116 @@ +{ + "index.md": "29488f37f0c90cb66e32023aa3869f6081dab42590def9196514de8e51e977de", + "introduction.md": "7159b93c7cf42ed98bd0a86ed2b0100884e1eae755e56d8392e9a4dfbe306dd0", + "app.md": "f1f4383a395605c4cd1c36314b61afeebc6369e8c624080ba4f005bd064db99d", + "app/arguments.md": "009c2438d897ea0f25d569d9b3b9654e55d1a01ad3c04bf8b4c512a1b9789f59", + "app/dependency-injection.md": "b89fd5d14b2e0180a5c29f31e79ecc4112d328d9efe89f4e5a3ca8caa88129f5", + "app/modules.md": "9d0c92e9db99c4e96c00152a51f69efa49c974860e33937aec6144d10795dc27", + "app/services.md": "9df90a136ffed203714a4427137ba9bfda6901eb77c28dfe0d8c305f135c06c3", + "app/events.md": "45af019cd3a41e6e5c4987b6d7980d5103afb20a889aba7c9a8d04cae73b3dba", + "app/logger.md": "467d21c830908bc4a49c3a3d80dd6c9207c3403977a693788990b04d89f2c8d6", + "app/configuration.md": "5cd9710e8c7037e4af0a43f47095332fca70a9c0a1e267f249d0f89e1d0c995e", + "framework.md": "7d23eb8115535150c547adc60d8677c9e74f7aa6d0f9ba2aa2c34dd3c61c1f72", + "framework/database.md": "3082875ba15ff97becd94966634dc57a4e3a96e0e09db1bd1a901faeecfc3a9f", + "framework/testing.md": "f22048a69772b752d7174645e25708288f67721ac4f16774e7c3786d9143691f", + "framework/deployment.md": "9f43e70a78caf28a58a76e6b6f4b654dca8e83fef9a5dfacd0544c9a40251ee0", + "framework/public.md": "014cf4e0702d543c6aa086a1256e024d63a06fe761f4352e06ba953a3ae5d39f", + "runtime-types.md": "ddb2b9e67054ede0ca0937044b4876882326ea421d7a9919633ae8187782bff7", + "runtime-types/getting-started.md": "437cb0b8ba80b036e023719d8bd56ad22d5414420199b9fa7561afba1f68b65a", + "runtime-types/types.md": "f8b43c9534f850990aee6e57dfcd5bffdaf087d953b2edc3f95dea5b9cc53b6d", + "runtime-types/reflection.md": "5b96135eeb9281f65ca5c365d4697868a31ce2a85b141e0283070ac3bec8dae9", + "runtime-types/serialization.md": "9d1d7adac33dc116244b6edb7cf37c88c7d7657c3692d3e354e04c48b71d7561", + "runtime-types/validation.md": "9ba4b9f65b3f2c882e1df373411652df407b1ee9300faed54d6f4b62b4a0f986", + "runtime-types/extend.md": "2b88f476dac2c88ffcd05719177b9c5e43281b65cd3925698df14483f3de4792", + "runtime-types/custom-serializer.md": "b3da230cee1630ea1eb7d89f7b3e241726c3b956dffb6a8a3f94da0a2ea8ef12", + "runtime-types/external-types.md": "216d15bd8b956f8821bd1c3c86571550beabe9b769237514b232b4dd8af72714", + "runtime-types/bytecode.md": "b22322df3dcc093da4d776a1b68d3d11ae5e8c8053723f785ad2a25c6a2e8d2e", + "dependency-injection.md": "c1ddf1c3c6dc56783c7e282605f5b3b0ea50f81cd8d614ff77a6459e3893f3d4", + "dependency-injection/getting-started.md": "4a381a82a5b5aa208c44d08334b6b7ae8ee129878f1a212b08230371cee99556", + "dependency-injection/providers.md": "64ae199092c89687a393ec3b50722c746e1048fb9aceed2c5f4d61150c43c573", + "dependency-injection/injection.md": "399be234a76ea3575f53a1c1bf04ab7368b4d9711b3811f5be69ccdb275da80c", + "dependency-injection/configuration.md": "9f84fa97310995897afafdf2b6b5376858ad22722d25a19562b1f70051a41009", + "dependency-injection/scopes.md": "7886d8e715ef18a5b058a89fe099cf4f5ce2947495c7f1fb4a8fe1908ec0cf59", + "filesystem.md": "97fc7c9f028efb934b9ad864135420007cd1910c7b8a7b3b8bdc86b0b8e90cdf", + "filesystem/app.md": "f7aa47e42604191de2d549f1f8e5e9582394e55da0c8d470147783b62a6a383d", + "filesystem/local.md": "e62660fe771d5c22c1414a055bc96e77a92da04e780f8853be9a5d327a2077b2", + "filesystem/memory.md": "67e061e990066271c4e6d92e1552319a8f1d3e2b5b97b2a26a9dc2e88858e906", + "filesystem/database.md": "8d1132ef3880a90e48e27598514ce6f75eed6328658e7c29b3af9509b8735859", + "filesystem/aws-s3.md": "989c857dbe659e4c6a765442143945402ed49db36248958a123c3d11649b4148", + "filesystem/ftp.md": "3400d186b86e1abb2823a0d64a143720099b2d5ba9c9e59e78ce1cc24d74df65", + "filesystem/sftp.md": "f54ebc5db81bf8cc4e3e21e922168db5456ba9912abbcb456a17d7603ffee4db", + "filesystem/google-storage.md": "c20d8abfc8e10c26844bc8a154ca8eacdf202c812cdde45911e30086943429cb", + "broker.md": "0e97c1ea8ba83e929555018e47a2e7ea9a4d951a2eda3fdd3c68ca2e83dc9c99", + "broker/cache.md": "cfbc3dc50876e5951c848a60a82c5b83d34dc351cb325a64850e6910a774ba34", + "broker/message-bus.md": "8fec0f694e08bd4e894ddafa8d97ce4183b8713d1d9be93dcb1bde4df7ac700c", + "broker/message-queue.md": "cbc54b3d2ea7aba249948f06d8f622defe7cfcae9ef5e0fa48f97b876e8346fb", + "broker/atomic-locks.md": "90703416ba1fec9f5c14eee4972ee78b983d126544737f2801188962741d87c8", + "broker/key-value.md": "da10a448f38971d72963500f4cfc06ad8253d79808d877453f6e5734cdf30070", + "http.md": "2c20d0e955afe136ada302e197f176255aac7d941760c208f0dc75c2f3cf258a", + "http/getting-started.md": "59605a5fe0ca617bb70e3fd0014340b9b57a88e8e91fac9bdc3eaf0a8f8f11c1", + "http/input-output.md": "c225ccfafb21816bd804a96110e69301baa0dbb57304baed8a7595b771f46f77", + "http/views.md": "62ce86b45d849b7946608aeea23f88f1078d74f971703709c98f8ad476944893", + "http/dependency-injection.md": "65d42f23a838af794cc43e60aaa2ae6cd3d72e2d0fc275065e65c6c639a6e534", + "http/events.md": "aa47d884f82e724f1dcb5b33b10624bef1e9a04b84f97c4db5a8d3f8da6bd33b", + "http/middleware.md": "9485a78862a08ff1de9cfd52b89f095676e70e058ec732b8e5617d96e961372a", + "http/security.md": "cde7cb77eb3b2aa51456d20d29a06ea7ab205b40ef9ad586dbfd136cd498019c", + "rpc.md": "cf15217c23ae52a86ab7dcf628c668d5d06b9720ef4ddb3717f02994a3d507f7", + "rpc/getting-started.md": "b09000d9e2d9dfe6527b8a42cbec75ae2206284d4395b75e21a8e1c596430ba9", + "rpc/dependency-injection.md": "c8e5218600779f3c3c4fcdccbe7ab8add343f8a8c7f2847dc3b80da96d1604e9", + "rpc/security.md": "790bb3ff8ef236250d29d16b55e8b685be273f52accb3a368be71eed4a8e639d", + "rpc/errors.md": "7785519ce38ddd349353e71bcc62bb5ba74582f52c17378508f4c6e9051609a9", + "rpc/transport.md": "9561d6ccbfeaf511b6a914fd3d7e6f4b84411f70938ad639665f113e469e1991", + "orm.md": "e922d5a0e28aa3753e5332418f2802f1ae62596d17ae96f46d3deca59c4e7c0b", + "orm/getting-started.md": "56e4f565f748aaf043ce8710fc5a58402fb5889366d1478a267e26628ff4e0f6", + "orm/entity.md": "5fccac031df6723dc0e6bdc9ef7ecd4268631b3b14854661d8ce6dc918f9959c", + "orm/session.md": "88c774506bcca58e5c272f59b844846ebe4efe5ab2e3a367528764b0dc5dfdee", + "orm/query.md": "540a355c2c1666c2a4d9e7c8cc202994b732b17408508fd6944dde1f62a99646", + "orm/transactions.md": "b843d35bab46190b24a79f05950b6a301349bf539c01aae7b90716f5d0153b41", + "orm/inheritance.md": "943f367861fcaebffe70d16c4d795a739b16537bed0ca602d0343e0684ebc0ae", + "orm/relations.md": "9cfd7b3516d704e091207b0d78995b26df6d5b88b1c8181ee021ee0d7767795a", + "orm/events.md": "220d9066075c3a82e354dc8e6fcf82db649e440b8c3df0c0d2722ac5bec9dcfd", + "orm/migrations.md": "2ee65d0ccd58635e9cd115a3abf83b2488ddc925a7d5f5b25d8844bcedc107e6", + "orm/orm-browser.md": "ee8940fa282c2d42861ce203524277f4672c880a3e79cffdc978c55e795cec1b", + "orm/raw-access.md": "55c6e5a40386cff597293293536be5b47b082122432156b50bf06b386d3bb694", + "orm/seeding.md": "92bad8e262e1afdc818b39e6aa91c50a9016ad37da273defb7caf575821e6539", + "orm/composite-primary-key.md": "155cb42a4dcd9deffaa67aa30369d86c37826a28740977fe1905ef0c61bb9129", + "orm/plugin-soft-delete.md": "afa2c7f42833bddda5f7e116a3e7eabfc7183d112812e6c4e8c3a802aca5c3ff", + "package/angular-ssr.md": "e7e74dfdd8f957e82061c449938c2e1288031b976921e54e28b4f0d35b5d491d", + "package/api-console.md": "82d5322b37c517da883f567a18d4f64ecbcd61570300ac337e5e3c1473359880", + "package/app.md": "e60ef9f596cc6df26f490350a0001a912a715d463c762ed09ca6bf796e4cb4b3", + "package/bench.md": "f7a06c685a2f05aa397a2f271bec2a98c729c4273591e6996381b420a57116df", + "package/broker.md": "e46abbefd581c101d77fdf35151eddeeba32074d24f6e7b737841c3cefa2b76f", + "package/broker-redis.md": "67bbf67a50265ada9755bf093cc3d566b8ff21dd10a2cd1f3ed5f589826aac1a", + "package/bson.md": "dbbadeca42bacda8a7cb2e7cce55e9627463537cf16de4458dd029163575063e", + "package/bun.md": "a5627944c66b654c3b64e380fa58acebf0a22f4b045ae218880d882973f457eb", + "package/core.md": "fabeee88c765e0a0713cfafa7cc502bd4f04fa4c69467a8a9985d7d74b5b963e", + "package/core-rxjs.md": "42784cd0fa8c85d0473753e1cee7b769bf03d4d220f18add821bfa402624619f", + "package/devtool.md": "9fa98e55994ae65e50032d11c9c01f7fb52fbe0dec22cc9b2b6d5dbbe67af122", + "package/event.md": "4bcb6f33e7b788d4f2db94f8871ccded00c6523cd80bcf593d732293d207e148", + "package/filesystem.md": "0c2634e9f2f938d1bec04d201e449642f2573c1e0790c1fb783dccea91b4fa63", + "package/filesystem-aws-s3.md": "40cfd7a7cd0d0f495d9e76f693792859accd6e56a92d6defbff0458cd55ee73d", + "package/filesystem-database.md": "61848eaf206556fbc5d317eb7a1286288bc48329ad209b7dc4ce6daaa1ce374f", + "package/filesystem-ftp.md": "fdbd9659eb784a5f7cc8219a59c647b7646c7de939e424fe23ea9c6fe0306d3a", + "package/filesystem-google.md": "549623afd3bfa25d793758893360902751a21b0b6ae900e27448c8cb2bc8593c", + "package/filesystem-sftp.md": "b2966556da65453da7b98a6900667b2fb97bab48c986bbdae667c172fec44b41", + "package/framework.md": "027c9d986ec7e32a3b93a857f4cdb80367d17b54faf59c7d1620d8a21f7c4628", + "package/http.md": "159c8f0553a1cd8f71598ef7128a78525b7ad55999b8b2270acbdd05435352fd", + "package/injector.md": "f01def47d678021bb90a48e20d7e94ce18e865422affcf39314d66d04218b11e", + "package/logger.md": "0336bedc41fda399389fbb9085032091312973f33e3ec9bd4ed46aa931469100", + "package/mongo.md": "359d07c8c17320d28840fbdfa0c3eb219f5eedd65b14866ab3ac64d25d502c6f", + "package/mysql.md": "0bde461414a0669797ff3c9c1c1ab6954dbb1bd6d1fd11a4d916fa8cb41ea683", + "package/orm.md": "2e622c77f4477c7e69f330b68b3253e3d54cf9ab4c124935269a677c31753919", + "package/orm-browser.md": "534cb7f207f14d425a3ccd03e2ad038137d3732ea1fd51a67019b315e02e766c", + "package/postgres.md": "9296b10815117f3f96af3171c36a554b8fb5033b4328d956338ce69adcb18d04", + "package/rpc.md": "c455f9ab0212e27b208b29948c9979feb9fd05143248474f91d5dce0b6c1d236", + "package/rpc-tcp.md": "4924f5e05bec8a288008315edef65bcc1ee5d940cc87b0126b82a9f188b78079", + "package/run.md": "16f7e3360ffcabe09bda174e6c8ed64055a869c41d2355c4d55344fde25ee411", + "package/sql.md": "8b4046cc206ff85449595ed7debce715e487097a3e9446f1841b7ef213c932dd", + "package/sqlite.md": "64fc4711e3557b9453300d2a99f01d69ea640d728483984ff35ea13142a79efb", + "package/stopwatch.md": "f8dc8d14b28ee81a00ac9c30f9278ce70ece11837dd132d47dfd0e70ed7fa8af", + "package/template.md": "135fe797c2a4309e2257bd118218927ce1b1d1265fa2297f5f02c75b4b62c5dc", + "package/topsort.md": "a0c2258b8859c5c7d864758f27699835d2017a630d3855eb08df4d4e36024def", + "package/type.md": "7d28ae8aca6dbbd0439bd7c778f5871d3ba7305660edeefa8fb7aa5ae80bfb3d", + "package/type-compiler.md": "819bcb710c7cc610238a69b37eb6809d693ccce84f3dc239642591d0cfcf9426", + "package/vite.md": "df1a5256d86a3560f3f0384122779255b9afe038c0c0eca4f705a23c336b51e2", + "package/workflow.md": "ebe03caf6abcd0ea512dd560ccf54c9fb34798b66c87780e6c24ce295cf8e76d" +} \ No newline at end of file diff --git a/website/src/translations/ko/basics.json b/website/src/translations/ko/basics.json new file mode 100644 index 000000000..3dea6ddf4 --- /dev/null +++ b/website/src/translations/ko/basics.json @@ -0,0 +1,160 @@ +{ + "Deepkit is a modular framework for TypeScript backend web applications.": "Deepkit는 TypeScript 백엔드 웹 애플리케이션을 위한 모듈식 프레임워크입니다.", + "Structured, scalable, and built for enterprise-grade architecture.": "구조적이며 확장 가능하고, 엔터프라이즈급 아키텍처를 위해 설계되었습니다.", + "Getting Started": "시작하기", + "View on GitHub": "GitHub에서 보기", + "Docs": "문서", + "Blog": "블로그", + "Chapters": "장", + "Overview": "개요", + "Introduction": "소개", + "App": "앱", + "Getting started": "시작하기", + "Arguments & Flags": "인수 및 플래그", + "Dependency Injection": "의존성 주입", + "Modules": "모듈", + "Services": "서비스", + "Events": "이벤트", + "Logger": "로거", + "Configuration": "설정", + "Framework": "프레임워크", + "Database": "데이터베이스", + "Testing": "테스트", + "Deployment": "배포", + "Public Assets": "공용 자산", + "Runtime Types": "런타임 타입", + "Type Annotations": "타입 주석", + "Reflection": "리플렉션", + "Serialization": "직렬화", + "Validation": "검증", + "Extend": "확장", + "Custom serializer": "사용자 정의 직렬화기", + "External Types": "외부 타입", + "Bytecode": "바이트코드", + "Providers": "프로바이더", + "Injection": "주입", + "Scopes": "스코프", + "Filesystem": "파일 시스템", + "Local": "로컬", + "Memory": "메모리", + "AWS S3": "AWS S3", + "FTP": "FTP", + "sFTP (SSH)": "sFTP (SSH)", + "Google Storage": "Google 스토리지", + "Broker": "브로커", + "Cache": "캐시", + "Message Bus": "메시지 버스", + "Message Queue": "메시지 큐", + "Atomic Locks": "원자적 잠금", + "Key Value": "키-값", + "HTTP": "HTTP", + "Input & Output": "입력 및 출력", + "Views": "뷰", + "Middleware": "미들웨어", + "Security": "보안", + "RPC": "RPC", + "Errors": "오류", + "Transport": "전송", + "Database ORM": "데이터베이스 ORM", + "Entity": "엔티티", + "Session": "세션", + "Query": "쿼리", + "Transaction": "트랜잭션", + "Inheritance": "상속", + "Relations": "관계", + "Migrations": "마이그레이션", + "ORM Browser": "ORM 브라우저", + "Raw Access": "원시 접근", + "Seeding": "시딩", + "Composite primary key": "복합 기본 키", + "Soft-Delete": "소프트 삭제", + "Desktop UI": "데스크톱 UI", + "Styles": "스타일", + "Adaptive Container": "적응형 컨테이너", + "Button": "버튼", + "Button group": "버튼 그룹", + "Dialog": "대화상자", + "Drag": "드래그", + "Dropdown": "드롭다운", + "Icons": "아이콘", + "Input": "입력", + "Menu": "메뉴", + "Slider": "슬라이더", + "Radio": "라디오", + "Select": "선택", + "Checkbox": "체크박스", + "List": "리스트", + "Table": "테이블", + "Tabs": "탭", + "Window": "창", + "Window Toolbar": "창 도구 모음", + "API": "API", + "Composition": "컴포지션", + "Command line interface (CLI) parser, config loader, dependency injection container, event system, modules system.": "명령줄 인터페이스(CLI) 파서, 설정 로더, 의존성 주입 컨테이너, 이벤트 시스템, 모듈 시스템.", + "App module that provides application/HTTP/RPC server, worker, debugger, integration tests.": "애플리케이션/HTTP/RPC 서버, 워커, 디버거, 통합 테스트를 제공하는 앱 모듈.", + "App module that provides HTTP server based on Node http module with validation and serialization.": "Node의 http 모듈을 기반으로 검증과 직렬화를 제공하는 HTTP 서버 앱 모듈.", + "Angular SSR": "Angular SSR", + "App module to integrate Angular SSR.": "Angular SSR을 통합하는 앱 모듈.", + "Infrastructure": "인프라", + "Remote procedure call (RPC) with binary encoding for WebSockets and TCP.": "WebSocket 및 TCP를 위한 이진 인코딩을 사용하는 원격 프로시저 호출(RPC).", + "RPC TCP": "RPC TCP", + "TCP server and client for Deepkit RPC.": "Deepkit RPC용 TCP 서버와 클라이언트.", + "Message broker with queues, pub/sub, key-value, 2 level cache, and distributed locks.": "큐, pub/sub, 키-값, 2단계 캐시, 분산 잠금을 지원하는 메시지 브로커.", + "Broker Redis": "브로커 Redis", + "Broker Redis adapter.": "브로커 Redis 어댑터.", + "Unified API to work with local and remote filesystems.": "로컬 및 원격 파일 시스템을 다루기 위한 통합 API.", + "Filesystem FTP": "파일 시스템 FTP", + "Fileystem FTP adapter.": "파일 시스템 FTP 어댑터.", + "Filesystem SFTP": "파일 시스템 SFTP", + "Fileystem SFTP (SSH) adapter.": "파일 시스템 SFTP(SSH) 어댑터.", + "Filesystem S3": "파일 시스템 S3", + "Fileystem S3 adapter.": "파일 시스템 S3 어댑터.", + "Filesystem Google": "파일 시스템 Google", + "Fileystem Google Storage adapter.": "파일 시스템 Google 스토리지 어댑터.", + "Filesystem Database": "파일 시스템 데이터베이스", + "Fileystem adapter for Deepkit ORM.": "Deepkit ORM용 파일 시스템 어댑터.", + "ORM/DBAL": "ORM/DBAL", + "Object-relational Mapper (ORM) and data access library (DAL). MongoDB, SQLite, Postgres, MySQL.": "객체-관계 매퍼(ORM)와 데이터 액세스 라이브러리(DAL). MongoDB, SQLite, Postgres, MySQL.", + "ORM MySQL": "ORM MySQL", + "MySQL Adapter for Deepkit ORM.": "Deepkit ORM용 MySQL 어댑터.", + "ORM Postgres": "ORM Postgres", + "PostgreSQL Adapter for Deepkit ORM.": "Deepkit ORM용 PostgreSQL 어댑터.", + "ORM SQLite": "ORM SQLite", + "SQLite Adapter for Deepkit ORM.": "Deepkit ORM용 SQLite 어댑터.", + "ORM Mongo": "ORM Mongo", + "MongoDB Adapter for Deepkit ORM.": "Deepkit ORM용 MongoDB 어댑터.", + "Fundamentals": "기본", + "Type": "타입", + "Runtime types with reflection, JSON serialization, validation, and type guards.": "리플렉션, JSON 직렬화, 검증, 타입 가드를 지원하는 런타임 타입.", + "Event": "이벤트", + "Async and synchronous event dispatcher.": "비동기 및 동기 이벤트 디스패처.", + "Dependency injection (DI) container with modules, config, scopes, and nominal type alias/interface support.": "모듈, 설정, 스코프, 명목적 타입 별칭/인터페이스를 지원하는 의존성 주입(DI) 컨테이너.", + "Template": "템플릿", + "HTML template engine based on JSX.": "JSX 기반의 HTML 템플릿 엔진.", + "Logger with scopes, colors, and custom transporter and formatter.": "스코프, 색상, 사용자 정의 전송자와 포매터를 지원하는 로거.", + "Workflow": "워크플로", + "Workflow engine / finite state machine.": "워크플로 엔진 / 유한 상태 기계.", + "Stopwatch": "스톱워치", + "Profile and collect execution time of code.": "코드의 실행 시간을 프로파일링하고 수집합니다.", + "Tools": "도구", + "Devtool": "개발 도구", + "Chrome devtools for Deepkit (RPC).": "Deepkit(RPC)용 Chrome 개발자 도구.", + "Angular Desktop UI library.": "Angular 데스크톱 UI 라이브러리.", + "Web user interface to manage ORM data.": "ORM 데이터를 관리하기 위한 웹 사용자 인터페이스.", + "API console": "API 콘솔", + "Bench": "벤치", + "Tools to run benchmarks and collect statistics.": "벤치마크를 실행하고 통계를 수집하는 도구.", + "Core": "코어", + "BSON": "BSON", + "BSON encoder and decoder.": "BSON 인코더와 디코더.", + "Core functions for working with JavaScript.": "JavaScript 작업을 위한 코어 함수.", + "Topsort": "위상 정렬", + "Topological sorting algorithm.": "위상 정렬 알고리즘.", + "Runtime": "런타임", + "Vite": "Vite", + "Vite plugin to use Deepkit runtime types.": "Deepkit 런타임 타입을 사용하기 위한 Vite 플러그인.", + "Bun": "Bun", + "Bun plugin to use Deepkit runtime types.": "Deepkit 런타임 타입을 사용하기 위한 Bun 플러그인.", + "Type Compiler": "타입 컴파일러", + "Type compiler as TypeScript plugin to make runtime types available.": "런타임 타입을 사용할 수 있도록 하는 TypeScript 플러그인 형태의 타입 컴파일러." +} diff --git a/website/src/translations/ko/documentation/app.md b/website/src/translations/ko/documentation/app.md new file mode 100644 index 000000000..c3fe817be --- /dev/null +++ b/website/src/translations/ko/documentation/app.md @@ -0,0 +1,174 @@ +# Deepkit App + +Deepkit App 추상화는 Deepkit 애플리케이션의 가장 기본적인 구성 요소입니다. 라이브러리를 단독으로 사용하지 않는다면 보통 여기서 애플리케이션을 만들기 시작합니다. +이는 Node.js 같은 런타임으로 실행하는 일반적인 TypeScript 파일입니다. 애플리케이션의 진입점이며, CLI 명령, 서비스, 구성, 이벤트 등을 정의하는 방법을 제공합니다. + +Command-line Interface(CLI) 프로그램은 텍스트 입력과 텍스트 출력을 통해 터미널로 상호작용하는 프로그램입니다. 이 방식의 장점은 로컬 또는 SSH 연결을 통해 터미널만 존재하면 된다는 점입니다. + +제공 기능: + +- CLI 명령 +- 모듈 시스템 +- 서비스 컨테이너 +- 의존성 주입 +- 이벤트 시스템 +- 로거 +- 설정 로더(env, dotenv, json) + +Deepkit의 명령은 DI 컨테이너에 완전한 접근 권한을 가지며 모든 프로바이더와 구성 옵션에 접근할 수 있습니다. CLI 명령의 인수와 옵션은 TypeScript 타입을 통한 매개변수 선언으로 제어되며 자동으로 직렬화 및 검증됩니다. + +`@deepkit/framework`를 사용하는 [Deepkit 프레임워크](./framework.md)는 이를 HTTP/RPC용 애플리케이션 서버, 디버거/프로파일러 등으로 확장합니다. + +## 간편 설치 + +가장 쉬운 시작 방법은 NPM init을 사용해 새 Deepkit 프로젝트를 생성하는 것입니다. + +```shell +npm init @deepkit/app@latest my-deepkit-app +```` + +이렇게 하면 모든 의존성과 기본 `app.ts` 파일이 포함된 새 폴더 `my-deepkit-app`가 생성됩니다. + +```sh +cd my-deepkit-app +npm run app +```` + +이 명령은 `ts-node`로 `app.ts` 파일을 실행하고 사용 가능한 명령을 보여줍니다. 여기서부터 시작해 자신만의 명령, 컨트롤러 등을 추가할 수 있습니다. + +## 수동 설치 + +Deepkit App은 [Deepkit 런타임 타입](./runtime-types.md)을 기반으로 하므로 모든 의존성을 설치합니다: + +```bash +mkdir my-project && cd my-project + +npm install typescript ts-node +npm install @deepkit/app @deepkit/type @deepkit/type-compiler +``` + +다음 명령을 실행하여 Deepkit의 type compiler가 `node_modules/typescript`에 설치된 TypeScript 패키지에 설치되었는지 확인합니다: + +```sh +./node_modules/.bin/deepkit-type-install +``` + +모든 peer dependency가 설치되었는지 확인하세요. 기본적으로 NPM 7+는 자동으로 설치합니다. + +애플리케이션을 컴파일하려면 TypeScript 컴파일러가 필요하며, 앱을 쉽게 실행하기 위해 `ts-node` 사용을 권장합니다. + +`ts-node`를 사용하지 않는 대안은 TypeScript 컴파일러로 소스 코드를 컴파일한 뒤 JavaScript 소스 코드를 직접 실행하는 것입니다. 이는 짧은 명령의 경우 실행 속도를 극적으로 높이는 장점이 있습니다. 그러나 컴파일러를 수동으로 실행하거나 watcher를 설정해야 하므로 워크플로에 추가 오버헤드가 생깁니다. 이런 이유로 이 문서의 모든 예제에서는 `ts-node`를 사용합니다. + +## 첫 번째 애플리케이션 + +Deepkit 프레임워크는 구성 파일이나 특별한 폴더 구조를 사용하지 않으므로 프로젝트를 원하는 대로 구성할 수 있습니다. 시작하는 데 필요한 유일한 두 파일은 TypeScript app.ts 파일과 TypeScript 구성 파일 tsconfig.json입니다. + +프로젝트 폴더에 다음 파일이 있도록 하는 것이 목표입니다: + +``` +. +├── app.ts +├── node_modules +├── package-lock.json +└── tsconfig.json +``` + +기본 tsconfig 파일을 설정하고 `reflection`을 `true`로 설정해 Deepkit의 type compiler를 활성화합니다. +이는 의존성 주입 컨테이너 및 기타 기능을 사용하기 위해 필요합니다. + +```json title=tsconfig.json +{ + "compilerOptions": { + "outDir": "./dist", + "experimentalDecorators": true, + "strict": true, + "esModuleInterop": true, + "target": "es2020", + "module": "CommonJS", + "moduleResolution": "node" + }, + "reflection": true, + "files": [ + "app.ts" + ] +} +``` + +```typescript title=app.ts +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +const app = new App(); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +이 코드에서 테스트 명령을 정의하고 `run()`을 사용해 직접 실행하는 새 앱을 만든 것을 볼 수 있습니다. 이 스크립트를 실행하면 앱이 시작됩니다. + +그리고 바로 실행합니다. + +```sh +$ ./node_modules/.bin/ts-node app.ts +VERSION + Node + +USAGE + $ ts-node app.ts [COMMAND] + +TOPICS + debug + migration Executes pending migration files. Use migration:pending to see which are pending. + server Starts the HTTP server + +COMMANDS + test +``` + +이제 테스트 명령을 실행하려면 다음 명령을 실행합니다. + +```sh +$ ./node_modules/.bin/ts-node app.ts test +Hello World +``` + +Deepkit에서는 이제 모든 작업이 이 `app.ts`를 통해 수행됩니다. 파일 이름을 원하는 대로 바꾸거나 더 만들어도 됩니다. 사용자 정의 CLI 명령, HTTP/RPC 서버, 마이그레이션 명령 등은 모두 이 진입점에서 시작됩니다. + +## 인수 및 플래그 + +Deepkit App은 Function 매개변수를 CLI 인수와 플래그로 자동 변환합니다. 매개변수의 순서는 CLI 인수의 순서를 결정합니다. + +매개변수는 임의의 TypeScript 타입이 될 수 있으며 자동으로 검증되고 역직렬화됩니다. + +자세한 내용은 [인수 및 플래그](./app/arguments.md) 장을 참조하세요. + +## 의존성 주입 + +Deepkit App은 서비스 컨테이너를 설정하고, 가져온 각 모듈에 대해 상위로부터 상속되는 자체 의존성 주입 컨테이너를 만듭니다. +다음 프로바이더를 기본으로 제공하며, 서비스, 컨트롤러, 이벤트 리스너에 자동으로 주입할 수 있습니다: + +- `Logger` 로그용 +- `EventDispatcher` 이벤트 처리용 +- `CliControllerRegistry` 등록된 CLI 명령용 +- `MiddlewareRegistry` 등록된 미들웨어용 +- `InjectorContext` 현재 인젝터 컨텍스트용 + +Deepkit 프레임워크를 가져오면 추가 프로바이더를 사용할 수 있습니다. 자세한 내용은 [Deepkit 프레임워크](./framework.md)를 참조하세요. + +## 종료 코드 + +종료 코드는 기본적으로 0이며, 이는 명령이 성공적으로 실행되었음을 의미합니다. 종료 코드를 변경하려면 execute 메서드 또는 명령 콜백에서 0이 아닌 숫자를 반환해야 합니다. + +```typescript + +@cli.controller('test') +export class TestCommand { + async execute() { + console.error('Error :('); + return 12; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/app/arguments.md b/website/src/translations/ko/documentation/app/arguments.md new file mode 100644 index 000000000..d1b41b46d --- /dev/null +++ b/website/src/translations/ko/documentation/app/arguments.md @@ -0,0 +1,316 @@ +# 인자와 플래그 + +명령의 터미널에서 전달하는 명령 인자는 `execute` 메서드 또는 함수의 일반적인 인자와 동일합니다. 이들은 자동으로 커맨드 라인 인자에 매핑됩니다. +매개변수를 선택적으로 표시하면 전달할 필요가 없습니다. 기본값이 있는 경우에도 전달할 필요가 없습니다. + +타입(string, number, union 등)에 따라 전달된 값은 자동으로 역직렬화되고 검증됩니다. + +```typescript +import { cli } from '@deepkit/app'; + +// 함수형 +new App().command('test', (name: string) => { + console.log('Hello', name); +}); + +// 클래스 +@cli.controller('test') +class TestCommand { + async execute(name: string) { + console.log('Hello', name); + } +} +``` + +이제 name 매개변수를 지정하지 않고 이 명령을 실행하면 오류가 발생합니다: + +```sh +$ ts-node app.ts test +RequiredArgsError: Missing 1 required arg: +name +``` + +`--help`를 사용하면 필요한 인자에 대한 더 많은 정보를 얻을 수 있습니다: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node-script app.ts test NAME +``` + +이름이 인자로 전달되면 명령이 실행되고 이름이 올바르게 전달됩니다. + +```sh +$ ts-node app.ts test "beautiful world" +Hello beautiful world +``` + +string, number, boolean, 문자열 리터럴, 이들의 유니온, 그리고 이들의 배열과 같은 모든 원시 매개변수 타입은 자동으로 CLI 인자로 사용되며 자동으로 검증 및 역직렬화됩니다. 매개변수의 순서가 CLI 인자의 순서를 결정합니다. 원하는 만큼 매개변수를 추가할 수 있습니다. + +복합 객체(Interface, Class, 객체 리터럴)가 정의되는 즉시 서비스 의존성으로 취급되며, 의존성 주입 컨테이너가 이를 해석하려고 시도합니다. 자세한 내용은 [의존성 주입](dependency-injection.md) 장을 참조하세요. + +## 플래그 + +플래그는 명령에 값을 전달하는 또 다른 방법입니다. 대부분 선택 사항이지만 반드시 그럴 필요는 없습니다. `Flag` 타입으로 데코레이션된 매개변수는 `--name value` 또는 `--name=value`로 전달할 수 있습니다. + +```typescript +import { Flag } from '@deepkit/app'; + +// 함수형 +new App().command('test', (id: number & Flag) => { + console.log('id', name); +}); + +// 클래스 +class TestCommand { + async execute(id: number & Flag) { + console.log('id', id); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + --id=id (required) +``` + +도움말 보기의 "OPTIONS"에서 `--id` 플래그가 필요함을 확인할 수 있습니다. 이 플래그를 올바르게 입력하면 명령이 이 값을 받습니다. + +```sh +$ ts-node app.ts test --id 23 +id 23 + +$ ts-node app.ts test --id=23 +id 23 +``` + +### 불리언 플래그 + +플래그는 특정 동작을 활성화하기 위해 값 없이도 사용할 수 있다는 장점이 있습니다. 매개변수가 선택적 boolean으로 표시되는 즉시 이 동작이 활성화됩니다. + +```typescript +import { Flag } from '@deepkit/app'; + +// 함수형 +new App().command('test', (remove: boolean & Flag = false) => { + console.log('delete?', remove); +}); + +// 클래스 +class TestCommand { + async execute(remove: boolean & Flag = false) { + console.log('delete?', remove); + } +} +``` + +```sh +$ ts-node app.ts test +delete? false + +$ ts-node app.ts test --remove +delete? true +``` + +### 다중 플래그 + +같은 플래그에 여러 값을 전달하려면, 플래그를 배열로 표시할 수 있습니다. + +```typescript +import { Flag } from '@deepkit/app'; + +// 함수형 +new App().command('test', (id: number[] & Flag = []) => { + console.log('ids', id); +}); + +// 클래스 +class TestCommand { + async execute(id: number[] & Flag = []) { + console.log('ids', id); + } +} +``` + +```sh +$ ts-node app.ts test +ids: [] + +$ ts-node app.ts test --id 12 +ids: [12] + +$ ts-node app.ts test --id 12 --id 23 +ids: [12, 23] +``` + +### 단일 문자 플래그 + +플래그를 한 글자로도 전달할 수 있도록 하려면 `Flag<{char: 'x'}>`를 사용할 수 있습니다. + +```typescript +import { Flag } from '@deepkit/app'; + +// 함수형 +new App().command('test', (output: string & Flag<{char: 'o'}>) => { + console.log('output: ', output); +}); + +// 클래스 +class TestCommand { + async execute(output: string & Flag<{char: 'o'}>) { + console.log('output: ', output); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + -o, --output=output (required) + + +$ ts-node app.ts test --output test.txt +output: test.txt + +$ ts-node app.ts test -o test.txt +output: test.txt +``` + +## 선택 사항 / 기본값 + +메서드/함수의 시그니처는 어떤 인자나 플래그가 선택 사항인지 정의합니다. 타입 시스템에서 매개변수가 선택적이면 사용자는 이를 제공하지 않아도 됩니다. + +```typescript + +// 함수형 +new App().command('test', (name?: string) => { + console.log('Hello', name || 'nobody'); +}); + +// 클래스 +class TestCommand { + async execute(name?: string) { + console.log('Hello', name || 'nobody'); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +기본값이 있는 매개변수도 동일합니다: + +```typescript +// 함수형 +new App().command('test', (name: string = 'body') => { + console.log('Hello', name); +}); + +// 클래스 +class TestCommand { + async execute(name: string = 'body') { + console.log('Hello', name); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +이는 플래그에도 동일하게 적용됩니다. + + +## 직렬화 / 검증 + +모든 인자와 플래그는 타입에 따라 자동으로 역직렬화되고 검증되며, 추가 제약 조건을 지정할 수 있습니다. + +즉, 숫자로 정의된 인자는 커맨드라인 인터페이스가 텍스트(문자열) 기반임에도, 컨트롤러에서는 항상 실제 숫자임이 보장됩니다. + +```typescript +// 함수형 +new App().command('test', (id: number) => { + console.log('id', id, typeof id); +}); + +// 클래스 +class TestCommand { + async execute(id: number) { + console.log('id', id, typeof id); + } +} +``` + +```sh +$ ts-node app.ts test 123 +id 123 number +``` + +추가 제약 조건은 `@deepkit/type`의 타입 애노테이션으로 정의할 수 있습니다. + +```typescript +import { Positive } from '@deepkit/type'; +// 함수형 +new App().command('test', (id: number & Positive) => { + console.log('id', id, typeof id); +}); + +// 클래스 +class TestCommand { + async execute(id: number & Positive) { + console.log('id', id, typeof id); + } +} +``` + +`id`의 `Postive` 타입은 양수만 허용됨을 나타냅니다. 사용자가 음수를 전달하면 코드는 전혀 실행되지 않고 오류 메시지가 표시됩니다. + +```sh +$ ts-node app.ts test -123 +Validation error in id: Number needs to be positive [positive] +``` + +이처럼 매우 간단한 추가 검증은 잘못된 입력에 대해 명령을 훨씬 더 견고하게 만듭니다. 자세한 내용은 [검증](../runtime-types/validation.md) 장을 참조하세요. + +## 설명 + +플래그나 인자를 설명하려면 `@description` 주석 데코레이터를 사용하세요. + +```typescript +import { Positive } from '@deepkit/type'; + +class TestCommand { + async execute( + /** @description 사용자의 식별자 */ + id: number & Positive, + /** @description 사용자를 삭제할까요? */ + remove: boolean = false + ) { + console.log('id', id, typeof id); + } +} +``` + +도움말 보기에서 이 설명은 해당 플래그 또는 인자 뒤에 표시됩니다: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test ID + +ARGUMENTS + ID The users identifier + +OPTIONS + --remove Delete the user? +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/app/configuration.md b/website/src/translations/ko/documentation/app/configuration.md new file mode 100644 index 000000000..cda497c33 --- /dev/null +++ b/website/src/translations/ko/documentation/app/configuration.md @@ -0,0 +1,265 @@ +# 설정 + +Deepkit 애플리케이션에서 모듈과 애플리케이션은 설정 옵션을 가질 수 있습니다. +예를 들어, 설정은 데이터베이스 URL, 비밀번호, IP 등으로 구성될 수 있습니다. 서비스, HTTP/RPC/CLI 컨트롤러, 템플릿 함수는 의존성 주입을 통해 이러한 설정 옵션을 읽을 수 있습니다. + +설정은 프로퍼티를 가진 클래스를 정의하여 지정할 수 있습니다. 이는 애플리케이션 전체의 설정을 타입 안전하게 정의하는 방법이며, 값은 자동으로 직렬화되고 검증됩니다. + +## 예제 + +```typescript +import { MinLength } from '@deepkit/type'; +import { App } from '@deepkit/app'; + +class Config { + pageTitle: string & MinLength<2> = 'Cool site'; + domain: string = 'example.com'; + debug: boolean = false; +} + +const app = new App({ + config: Config +}); + + +app.command('print-config', (config: Config) => { + console.log('config', config); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Hello from Cool site via example.com +``` + +설정 로더가 사용되지 않으면 기본값이 사용됩니다. 설정을 변경하려면 `app.configure({domain: 'localhost'})` 메서드를 사용하거나 환경 설정 로더를 사용할 수 있습니다. + +## 설정 값 설정 + +기본적으로 값은 덮어쓰지 않으므로 기본값이 사용됩니다. 설정 값을 지정하는 방법은 여러 가지가 있습니다. + +* `app.configure({})`를 통해 +* 각 옵션에 대한 환경 변수 +* JSON을 통한 환경 변수 +* dotenv 파일 + +여러 방법을 동시에 사용해 설정을 로드할 수 있습니다. 호출 순서가 중요합니다. + +### 환경 변수 + +각 설정 옵션을 자체 환경 변수로 설정할 수 있도록 하려면 `loadConfigFromEnv`를 사용합니다. 기본 접두사는 `APP_`이지만 변경할 수 있습니다. 또한 `.env` 파일을 자동으로 로드합니다. 기본적으로 대문자 명명 전략을 사용하지만, 이 또한 변경할 수 있습니다. + +위의 `pageTitle`과 같은 설정 옵션의 값을 변경하려면 `APP_PAGE_TITLE="Other Title"`을 사용할 수 있습니다. + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({prefix: 'APP_'}) + .run(); +``` + +```sh +APP_PAGE_TITLE="Other title" ts-node app.ts server:start +``` + +### JSON 환경 변수 + +하나의 환경 변수로 여러 설정 옵션을 변경하려면 `loadConfigFromEnvVariable`을 사용합니다. 첫 번째 인수는 환경 변수의 이름입니다. + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnvVariable('APP_CONFIG') + .run(); +``` + +```sh +APP_CONFIG='{"pageTitle": "Other title"}' ts-node app.ts server:start +``` + +### DotEnv 파일 + +dotenv 파일을 통해 여러 설정 옵션을 변경하려면 `loadConfigFromEnv`를 사용합니다. 첫 번째 인수는 dotenv의 경로(`cwd` 기준) 또는 여러 경로입니다. 배열인 경우, 존재하는 파일이 발견될 때까지 각 경로를 시도합니다. + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({envFilePath: ['production.dotenv', 'dotenv']}) + .run(); +``` + +```sh +$ cat dotenv +APP_PAGE_TITLE=Other title +$ ts-node app.ts server:start +``` + +### 모듈 설정 + +가져온 각 모듈은 모듈 이름을 가질 수 있습니다. 이 이름은 위에서 사용한 설정 경로에 사용됩니다. + +예를 들어, 환경 변수를 통한 설정에서 `FrameworkModule`의 옵션 port에 대한 경로는 `FRAMEWORK_PORT`입니다. 모든 이름은 기본적으로 대문자로 작성됩니다. 접두사 `APP_`가 사용되면 포트는 다음과 같이 변경할 수 있습니다. + +```sh +$ APP_FRAMEWORK_PORT=9999 ts-node app.ts server:start +2021-06-12T18:59:26.363Z [LOG] Start HTTP server, using 1 workers. +2021-06-12T18:59:26.365Z [LOG] HTTP MyWebsite +2021-06-12T18:59:26.366Z [LOG] GET / helloWorld +2021-06-12T18:59:26.366Z [LOG] HTTP listening at http://localhost:9999/ +``` + +dotenv 파일에서도 `APP_FRAMEWORK_PORT=9999`가 됩니다. + +반면에 `loadConfigFromEnvVariable('APP_CONFIG')`를 통한 JSON 환경 변수에서는 실제 설정 클래스의 구조를 따릅니다. `framework`는 객체가 됩니다. + +```sh +$ APP_CONFIG='{"framework": {"port": 9999}}' ts-node app.ts server:start +``` + +이는 모든 모듈에 대해 동일하게 작동합니다. 애플리케이션 설정 옵션(`new App`)에는 모듈 접두사가 필요하지 않습니다. + + +## 설정 클래스 + +```typescript +import { MinLength } from '@deepkit/type'; + +export class Config { + title!: string & MinLength<2>; //필수 항목이 되며 값을 제공해야 합니다 + host?: string; + + debug: boolean = false; //기본값도 지원됩니다 +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} +``` + +설정 옵션의 값은 모듈의 생성자에서, `.configure()` 메서드를 통해, 또는 설정 로더(예: 환경 변수 로더)를 통해 제공될 수 있습니다. + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [new MyModule({title: 'Hello World'})], +}).run(); +``` + +가져온 모듈의 설정 옵션을 동적으로 변경하려면 `process` 훅을 사용할 수 있습니다. 이는 현재 모듈 설정 또는 다른 모듈 인스턴스 정보에 따라 설정 옵션을 전달하거나 가져온 모듈을 설정하기에 좋은 위치입니다. + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} +``` + +애플리케이션 수준에서는 약간 다르게 동작합니다: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +루트 애플리케이션 모듈이 일반 모듈에서 만들어진 경우에도, 일반 모듈과 유사하게 동작합니다. + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## 설정 값 읽기 + +서비스에서 설정 옵션을 사용하려면 일반적인 의존성 주입을 사용할 수 있습니다. 전체 설정 객체, 단일 값, 또는 설정의 일부만 주입하는 것이 가능합니다. + +### 부분 + +설정 값의 하위 집합만 주입하려면 `Pick` 타입을 사용합니다. + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private config: Pick<Config, 'title' | 'host'}) { + } + + getTitle() { + return this.config.title; + } +} + + +//단위 테스트에서는 다음과 같이 인스턴스화할 수 있습니다 +new MyService({title: 'Hello', host: '0.0.0.0'}); + +//또는 타입 별칭을 사용할 수 있습니다 +type MyServiceConfig = Pick<Config, 'title' | 'host'}; +export class MyService { + constructor(private config: MyServiceConfig) { + } +} +``` + +### 단일 값 + +단일 값만 주입하려면 인덱스 접근 연산자를 사용합니다. + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private title: Config['title']) { + } + + getTitle() { + return this.title; + } +} +``` + +### 전체 + +모든 설정 값을 주입하려면 클래스를 의존성으로 사용합니다. + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private config: Config) { + } + + getTitle() { + return this.config.title; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/app/dependency-injection.md b/website/src/translations/ko/documentation/app/dependency-injection.md new file mode 100644 index 000000000..cac181f96 --- /dev/null +++ b/website/src/translations/ko/documentation/app/dependency-injection.md @@ -0,0 +1,37 @@ +# 의존성 주입 + +모든 커맨드는 의존성 주입 컨테이너에 완전하게 접근할 수 있습니다. 커맨드나 컨트롤러의 생성자에서 의존성을 정의하면 의존성 주입 컨테이너가 이를 해결하려고 시도합니다. + +자세한 내용은 [의존성 주입](../dependency-injection.md) 장을 참조하세요. + +```typescript +import { App, cli } from '@deepkit/app'; +import { Logger, ConsoleTransport } from '@deepkit/logger'; + +new App({ + providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport])}], +}).command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); +``` + +```typescript +@cli.controller('test', { + description: 'My super first command' +}) +class TestCommand { + constructor(protected logger: Logger) { + } + + async execute() { + this.logger.log('Hello World!'); + } +} + +new App({ + providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport]}], + controllers: [TestCommand] +}).run(); +``` + +원하는 만큼의 의존성을 정의할 수 있습니다. 의존성 주입 컨테이너가 이를 자동으로 해결합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/app/events.md b/website/src/translations/ko/documentation/app/events.md new file mode 100644 index 000000000..640a946e9 --- /dev/null +++ b/website/src/translations/ko/documentation/app/events.md @@ -0,0 +1,248 @@ +# 이벤트 시스템 + +이벤트 시스템은 동일한 프로세스 내의 애플리케이션 컴포넌트들이 이벤트를 보내고 수신함으로써 서로 통신할 수 있도록 합니다. 이는 서로를 직접 알지 못할 수도 있는 함수들 간의 메시지 교환을 촉진하여 코드 모듈화를 돕습니다. + +애플리케이션 또는 라이브러리는 동작 중 특정 지점에서 추가 함수를 실행할 기회를 제공합니다. 이러한 추가 함수는 스스로를 "이벤트 리스너"로 등록합니다. + +이벤트는 다양한 형태를 가질 수 있습니다: + +- 애플리케이션이 시작하거나 종료된다. +- 새 사용자 생성 또는 삭제가 발생한다. +- 에러가 발생한다. +- 새 HTTP 요청을 수신한다. + +Deepkit Framework와 연관 라이브러리는 사용자가 수신하고 반응할 수 있는 다양한 이벤트를 제공합니다. 또한 사용자는 필요한 만큼의 사용자 정의 이벤트를 생성하여 애플리케이션을 모듈식으로 확장할 수 있습니다. + +## 사용법 + +Deepkit 앱을 사용한다면, 이벤트 시스템은 이미 포함되어 있으며 바로 사용할 수 있습니다. + +```typescript +import { App, onAppExecute } from '@deepkit/app'; + +const app = new App(); + +app.listen(onAppExecute, async (event) => { + console.log('MyEvent triggered!'); +}); + +app.run(); +``` + +이벤트는 `listen()` 메서드를 사용하거나 `@eventDispatcher.listen` 데코레이터를 사용하는 클래스를 통해 등록할 수 있습니다: + +```typescript +import { App, onAppExecute } from '@deepkit/app'; +import { eventDispatcher } from '@deepkit/event'; + +class MyListener { + @eventDispatcher.listen(onAppExecute) + onMyEvent(event: typeof onAppExecute.event) { + console.log('MyEvent triggered!'); + } +} + +const app = new App({ + listeners: [MyListener], +}); +app.run(); +``` + +## 이벤트 토큰 + +Deepkit의 이벤트 시스템의 핵심에는 이벤트 토큰(Event Token)이 있습니다. 이는 이벤트 ID와 이벤트의 타입을 모두 지정하는 고유한 객체입니다. 이벤트 토큰은 두 가지 주요 목적을 수행합니다: + +- 이벤트를 트리거하는 역할을 한다. +- 자신이 트리거하는 이벤트를 수신한다. + +이벤트 토큰을 사용하여 이벤트가 시작되면 해당 토큰의 소유자는 사실상 이벤트의 소스(발행자)로 인식됩니다. 토큰은 이벤트와 연관된 데이터를 결정하고 비동기 이벤트 리스너를 사용할 수 있는지 여부를 지정합니다. + +```typescript +import { EventToken } from '@deepkit/event'; + +const MyEvent = new EventToken('my-event'); + +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); + +//앱 참조를 통해 트리거 +await app.dispatch(MyEvent); + +//또는 EventDispatcher 사용, App의 DI 컨테이너가 자동으로 주입해줍니다 +app.command('test', async (dispatcher: EventDispatcher) => { + await dispatcher.dispatch(MyEvent); +}); +``` + +### 사용자 정의 이벤트 데이터 생성: + +@deepkit/event의 `DataEventToken` 사용: + +```typescript +import { DataEventToken } from '@deepkit/event'; + +class User { +} + +const MyEvent = new DataEventToken<User>('my-event'); +``` + +BaseEvent 확장: + +```typescript +class MyEvent extends BaseEvent { + user: User = new User; +} + +const MyEventToken = new EventToken<MyEvent>('my-event'); +``` + +## 함수형 리스너 + +함수형 리스너를 사용하면 간단한 함수 콜백을 디스패처에 직접 등록할 수 있습니다. 사용 방법은 다음과 같습니다: + +```typescript +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +``` + +`logger: Logger`와 같은 추가 인수를 도입하고자 한다면, Deepkit의 런타임 타입 리플렉션 덕분에 의존성 주입 시스템이 자동으로 주입합니다. + +```typescript +app.listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); +}); +``` + +첫 번째 인수는 반드시 이벤트 자체여야 합니다. 이 인수는 생략할 수 없습니다. + +`@deepkit/app`를 사용한다면 app.listen()을 통해 함수형 리스너를 등록할 수도 있습니다. + +```typescript +import { App } from '@deepkit/app'; + +new App() + .listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## 클래스 기반 리스너 + +클래스 리스너는 데코레이터로 장식된 클래스입니다. 이는 이벤트를 수신하는 구조화된 방법을 제공합니다. + +```typescript +import { App } from '@deepkit/app'; + +class MyListener { + @eventDispatcher.listen(UserAdded) + onUserAdded(event: typeof UserAdded.event) { + console.log('User added!', event.user.username); + } +} + +new App({ + listeners: [MyListener], +}).run(); +``` + +클래스 리스너의 경우, 의존성 주입은 메서드 인수나 생성자를 통해 동작합니다. + +## 의존성 주입 + +Deepkit의 이벤트 시스템은 강력한 의존성 주입 메커니즘을 제공합니다. 함수형 리스너를 사용할 때는 런타임 타입 리플렉션 시스템 덕분에 추가 인수가 자동으로 주입됩니다. 마찬가지로 클래스 기반 리스너도 생성자나 메서드 인수를 통해 의존성 주입을 지원합니다. + +예를 들어, 함수형 리스너의 경우 `logger: Logger`와 같은 인수를 추가하면 함수가 호출될 때 적절한 Logger 인스턴스가 자동으로 제공됩니다. + +```typescript +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +new App() + .listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## 이벤트 전파 + +모든 이벤트 객체에는 stop() 함수가 함께 제공되어 이벤트의 전파를 제어할 수 있습니다. 이벤트가 중단되면(추가된 순서상) 이후의 리스너는 실행되지 않습니다. 이는 특정 조건에서 이벤트 처리를 중단해야 하는 상황에 특히 유용하며, 이벤트의 실행과 처리에 대한 세밀한 제어를 제공합니다. + +예: + +```typescript +dispatcher.listen(MyEventToken, (event) => { + if (someCondition) { + event.stop(); + } + // 추가 처리 +}); +``` + +Deepkit 프레임워크의 이벤트 시스템을 통해 개발자는 모듈식이고 확장 가능하며 유지 보수가 쉬운 애플리케이션을 손쉽게 만들 수 있습니다. 이벤트 시스템을 이해하면 특정 발생이나 조건에 따라 애플리케이션의 동작을 유연하게 조정할 수 있습니다. + +## 프레임워크 이벤트 + +Deepkit Framework 자체에는 수신할 수 있는 애플리케이션 서버의 여러 이벤트가 있습니다. + +_함수형 리스너_ + +```typescript +import { onServerMainBootstrap } from '@deepkit/framework'; +import { onAppExecute } from '@deepkit/app'; + +new App({ + imports: [new FrameworkModule] +}) + .listen(onAppExecute, (event) => { + console.log('Command about to execute'); + }) + .listen(onServerMainBootstrap, (event) => { + console.log('Server started'); + }) + .run(); +``` + +| 이름 | 설명 | +|-----------------------------|------------------------------------------------------------------------------------------------------| +| onServerBootstrap | 애플리케이션 서버 부트스트랩 시 한 번만 호출됨(메인 프로세스 및 워커 모두). | +| onServerBootstrapDone | 애플리케이션 서버가 시작되는 즉시, 애플리케이션 서버 부트스트랩에 대해 한 번만 호출됨(메인/워커). | +| onServerMainBootstrap | 애플리케이션 서버 부트스트랩 시 한 번만 호출됨(메인 프로세스). | +| onServerMainBootstrapDone | 애플리케이션 서버가 시작되는 즉시, 애플리케이션 서버 부트스트랩에 대해 한 번만 호출됨(메인 프로세스).| +| onServerWorkerBootstrap | 애플리케이션 서버 부트스트랩 시 한 번만 호출됨(워커 프로세스). | +| onServerWorkerBootstrapDone | 애플리케이션 서버가 시작되는 즉시, 애플리케이션 서버 부트스트랩에 대해 한 번만 호출됨(워커 프로세스).| +| onServerShutdownEvent | 애플리케이션 서버가 종료될 때 호출됨(마스터 프로세스 및 각 워커). | +| onServerMainShutdown | 메인 프로세스에서 애플리케이션 서버가 종료될 때 호출됨. | +| onServerWorkerShutdown | 워커 프로세스에서 애플리케이션 서버가 종료될 때 호출됨. | +| onAppExecute | 명령이 실행되기 직전. | +| onAppExecuted | 명령이 성공적으로 실행되었을 때. | +| onAppError | 명령 실행에 실패했을 때 | +| onAppShutdown | 애플리케이션이 종료되기 직전. | + +## 저수준 API + +아래는 @deepkit/event에서 제공하는 저수준 API의 예시입니다. Deepkit App을 사용할 때는 이벤트 리스너가 EventDispatcher를 통해 직접 등록되지 않고 모듈을 통해 등록됩니다. 하지만 원한다면 저수준 API를 그대로 사용할 수도 있습니다. + +```typescript +import { EventDispatcher, EventToken } from '@deepkit/event'; + +//첫 번째 인수는 의존성 주입을 위한 의존성 해결을 수행하기 위한 인젝터 컨텍스트일 수 있습니다 +const dispatcher = new EventDispatcher(); +const MyEvent = new EventToken('my-event'); + +dispatcher.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +dispatcher.dispatch(MyEvent); + +``` + +### 설치 + +이벤트 시스템은 @deepkit/app에 포함되어 있습니다. 단독으로 사용하려면 수동으로 설치할 수 있습니다: + +설치 방법은 [이벤트](../event.md)를 참고하세요. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/app/logger.md b/website/src/translations/ko/documentation/app/logger.md new file mode 100644 index 000000000..13a44e38c --- /dev/null +++ b/website/src/translations/ko/documentation/app/logger.md @@ -0,0 +1,130 @@ +# 로거 + +Deepkit Logger는 정보를 로깅하는 데 사용할 수 있는 기본 Logger 클래스를 포함한 독립 실행형 라이브러리입니다. 이 클래스는 Deepkit 애플리케이션의 의존성 주입(Dependency Injection) 컨테이너에서 자동으로 사용 가능합니다. + +`Logger` 클래스에는 여러 메서드가 있으며, 각각은 `console.log`와 동일하게 동작합니다. + +| 이름 | 로그 레벨 | 레벨 ID | +|------------------|----------------------|---------| +| logger.error() | 오류 | 1 | +| logger.warning() | 경고 | 2 | +| logger.log() | 기본 로그 | 3 | +| logger.info() | 특별 정보 | 4 | +| logger.debug() | 디버그 정보 | 5 | + + +기본적으로 로거는 `info` 레벨을 가지며, 즉 info 메시지 이상만 처리합니다(즉, log, warning, error는 처리하지만 debug는 처리하지 않음). 로그 레벨을 변경하려면 예를 들어 `logger.level = 5`를 호출하세요. + +## 애플리케이션에서 사용 + +Deepkit 애플리케이션에서 로거를 사용하려면 서비스나 컨트롤러에 `Logger`를 간단히 주입하면 됩니다. + +```typescript +import { Logger } from '@deepkit/logger'; +import { App } from '@deepkit/app'; + +const app = new App(); +app.command('test', (logger: Logger) => { + logger.log('This is a <yellow>log message</yellow>'); +}); + +app.run(); +``` + +## 색상 + +로거는 컬러 로그 메시지를 지원합니다. 컬러로 표시하려는 텍스트를 감싸는 XML 태그를 사용하여 색상을 지정할 수 있습니다. + +```typescript +const username = 'Peter'; +logger.log(`Hi <green>${username}</green>`); +``` + +색상을 지원하지 않는 트랜스포터의 경우 색상 정보는 자동으로 제거됩니다. 기본 트랜스포터(`ConsoleTransport`)에서는 색상이 표시됩니다. 다음 색상을 사용할 수 있습니다: `black`, `red`, `green`, `blue`, `cyan`, `magenta`, `white`, `grey`/`gray`. + +## 트랜스포터 + +하나의 트랜스포터 또는 여러 트랜스포터를 구성할 수 있습니다. Deepkit 애플리케이션에서는 `ConsoleTransport` 트랜스포터가 자동으로 구성됩니다. 추가 트랜스포터를 구성하려면 [설정 호출](dependency-injection.md#di-setup-calls)을 사용하세요: + +```typescript +import { Logger, LoggerTransport } from '@deepkit/logger'; + +export class MyTransport implements LoggerTransport { + write(message: string, level: LoggerLevel, rawMessage: string) { + process.stdout.write(JSON.stringify({message: rawMessage, level, time: new Date}) + '\n'); + } + + supportsColor() { + return false; + } +} + +new App() + .setup((module, config) => { + module.configureProvider<Logger>(v => v.addTransport(new MyTransport)); + }) + .run(); +``` + +모든 트랜스포터를 새로운 트랜스포터 집합으로 교체하려면 `setTransport`를 사용하세요: + +```typescript +import { Logger } from '@deepkit/logger'; + +new App() +.setup((module, config) => { + module.configureProvider<Logger>(v => v.setTransport([new MyTransport])); +}) +.run(); +``` + +```typescript +import { Logger, JSONTransport } from '@deepkit/logger'; + +new App() + .setup((module, config) => { + module.configureProvider<Logger>(v => v.setTransport([new JSONTransport])); + }) + .run(); +``` + +## 스코프드 로거 + +스코프드 로거는 각 로그 항목에 임의의 영역 이름을 추가하여, 해당 로그 항목이 애플리케이션의 어떤 하위 영역에서 발생했는지 파악하는 데 도움이 됩니다. + +```typescript +const scopedLogger = logger.scoped('database'); +scopedLogger.log('Query', query); +``` + +서비스에 스코프드 로거를 주입할 때 사용할 수 있는 `ScopedLogger` 타입도 있습니다. + +```typescript +import { ScopedLogger } from '@deepkit/logger'; + +class MyService { + constructor(protected logger: ScopedLogger) {} + doSomething() { + this.logger.log('This is wild'); + } +} +``` + +이제 스코프드 로거의 모든 메시지에는 `MyService` 스코프 이름이 접두사로 붙습니다. + +## 포맷터 + +포맷터를 사용하면 메시지 형식을 변경할 수 있으며, 예를 들어 타임스탬프를 추가할 수 있습니다. 애플리케이션이 `server:start`로 시작될 때 다른 포맷터가 없으면 `DefaultFormatter`가 자동으로 추가됩니다(타임스탬프, 범위, 로그 레벨을 추가). + +## 컨텍스트 데이터 + +로그 항목에 컨텍스트 데이터를 추가하려면 마지막 인자로 간단한 객체 리터럴을 추가하세요. 최소 두 개 이상의 인자를 가진 로그 호출만 컨텍스트 데이터를 포함할 수 있습니다. + +```typescript +const query = 'SELECT *'; +const user = new User; +logger.log('Query', {query, user}); // 마지막 인자는 컨텍스트 데이터 +logger.log('Another', 'wild log entry', query, {user}); // 마지막 인자는 컨텍스트 데이터 + +logger.log({query, user}); // 이는 컨텍스트 데이터로 처리되지 않습니다. +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/app/modules.md b/website/src/translations/ko/documentation/app/modules.md new file mode 100644 index 000000000..5db5b6735 --- /dev/null +++ b/website/src/translations/ko/documentation/app/modules.md @@ -0,0 +1,516 @@ +# 모듈 + +Deepkit은 고도로 모듈화되어 있어 애플리케이션을 여러 유용한 모듈로 쉽게 분할할 수 있습니다. 각 모듈은 자체 의존성 주입 하위 컨테이너(모든 상위 provider를 상속), 구성(configuration), 명령(commands) 등을 가집니다. +[시작하기](../framework.md) 장에서 이미 하나의 모듈(루트 모듈)을 만들었습니다. `new App`은 모듈과 거의 동일한 인자를 받는데, 이는 백그라운드에서 자동으로 루트 모듈을 생성하기 때문입니다. + +애플리케이션을 서브 모듈로 분할할 계획이 없거나, 모듈을 패키지로 만들어 다른 사람에게 제공할 계획이 없다면 이 장은 건너뛰어도 됩니다. + +모듈은 클래스 모듈 또는 함수형 모듈로 정의할 수 있습니다. + +```typescript title=클래스 모듈 +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + //new App({})와 동일한 옵션 + providers: [MyService] +}) { +} +``` + +```typescript title=함수형 모듈 +import { AppModule } from '@deepkit/app'; + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addProvider(MyService); + }; +} +``` + +이 모듈은 애플리케이션이나 다른 모듈에 가져와(import) 사용할 수 있습니다. + +```typescript +import { MyModule, myModule } from './module.ts' + +new App({ + imports: [ + new MyModule(), //클래스 모듈 import + myModule(), //함수형 모듈 import + ] +}).run(); +``` + +이제 `App`과 동일하게 이 모듈에 기능을 추가할 수 있습니다. `createModule`의 인자는 동일하지만, 모듈 정의에서는 imports를 사용할 수 없습니다. +함수형 라우트의 경우 `AppModule`의 메서드를 사용하여 자체 옵션에 따라 동적으로 구성할 수 있습니다. + +HTTP/RPC/CLI 컨트롤러, 서비스, 구성, 이벤트 리스너 및 다양한 모듈 훅을 추가하여 모듈을 더욱 동적으로 만드세요. + +## 컨트롤러 + +모듈은 다른 모듈에 의해 처리되는 컨트롤러를 정의할 수 있습니다. 예를 들어 `@deepkit/http` 패키지의 데코레이터가 있는 컨트롤러를 추가하면, 해당 `HttpModule` 모듈이 이를 감지하여 라우터에 발견된 라우트를 등록합니다. 하나의 컨트롤러에는 이러한 데코레이터가 여러 개 있을 수 있습니다. 이러한 데코레이터를 제공하는 모듈 작성자가 컨트롤러를 어떻게 처리할지는 그에게 달려 있습니다. + +Deepkit에는 이러한 컨트롤러를 처리하는 세 가지 패키지(HTTP, RPC, CLI)가 있습니다. 자세한 내용은 각 장을 참고하세요. 아래는 HTTP 컨트롤러의 예시입니다: + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +class MyHttpController { + @http.GET('/hello) + hello() { + return 'Hello world!'; + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] +}) { +} + + +//App에서도 동일하게 가능 +new App({ + controllers: [MyHttpController] +}).run(); +``` + +## Provider + +애플리케이션의 `providers` 섹션에 provider를 정의하면 애플리케이션 전반에서 접근할 수 있습니다. 그러나 모듈의 경우 이러한 provider는 해당 모듈의 의존성 주입 하위 컨테이너에 자동으로 캡슐화됩니다. 다른 모듈이나 애플리케이션에서 사용할 수 있도록 하려면 각 provider를 수동으로 export해야 합니다. + +provider 동작에 대해 더 알아보려면 [의존성 주입](../dependency-injection.md) 장을 참조하세요. + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +export class HelloWorldService { + helloWorld() { + return 'Hello there!'; + } +} + +class MyHttpController { + constructor(private helloService: HelloWorldService) { + } + + @http.GET('/hello) + hello() { + return this.helloService.helloWorld(); + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addController(MyHttpController); + module.addProvider(HelloWorldService); + }; +} + +//App에서도 동일하게 가능 +new App({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}).run(); +``` + +사용자가 이 모듈을 import하더라도, `MyModule`의 하위 의존성 주입 컨테이너에 캡슐화되어 있기 때문에 `HelloWorldService`에 접근할 수 없습니다. + +## 내보내기 + +import하는 쪽의 모듈에서 provider를 사용할 수 있도록 하려면 `exports`에 provider의 토큰을 포함하면 됩니다. 이는 본질적으로 provider를 한 단계 위로, 즉 상위 모듈(가져오는 쪽)의 의존성 주입 컨테이너로 이동시킵니다. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + exports: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addExport(HelloWorldService); + }; +} +``` + +`FactoryProvider`, `UseClassProvider` 등 다른 provider를 사용하는 경우에도, exports에는 클래스 타입만 사용해야 합니다. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] + providers: [ + { provide: HelloWorldService, useValue: new HelloWorldService } + ], + exports: [HelloWorldService], +}) { +} +``` + +이제 해당 모듈을 import하고, 애플리케이션 코드에서 export된 서비스를 사용할 수 있습니다. + +```typescript +import { App } from '@deepkit/app'; +import { cli, Command } from '@deepkit/app'; +import { HelloWorldService, MyModule } from './my-module'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected helloWorld: HelloWorldService) { + } + + async execute() { + this.helloWorld.helloWorld(); + } +} + +new App({ + controllers: [TestCommand], + imports: [ + new MyModule(), + ] +}).run(); +``` + +자세한 내용은 [의존성 주입](../dependency-injection.md) 장을 참조하세요. + + +### 구성 스키마 + +모듈은 타입 안전한 구성 옵션을 가질 수 있습니다. 이 옵션 값은 클래스 참조 또는 `Partial<Config, 'url'>` 같은 타입 함수만으로 해당 모듈의 서비스에 부분적으로 또는 완전히 주입할 수 있습니다. 구성 스키마를 정의하려면 프로퍼티가 있는 클래스를 작성하세요. + +```typescript +export class Config { + title!: string; //필수이며 제공되어야 함 + host?: string; //옵션 + + debug: boolean = false; //기본값도 지원됩니다 +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.setConfigDefinition(Config).configure(options); + }; +} +``` + +구성 옵션 값은 모듈의 생성자, `.configure()` 메서드, 또는 구성 로더(예: 환경 변수 로더)를 통해 제공할 수 있습니다. + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [ + new MyModule({title: 'Hello World'}), + myModule({title: 'Hello World'}), + ], +}).run(); +``` + +import된 모듈의 구성 옵션을 동적으로 변경하려면 `process` 모듈 훅을 사용할 수 있습니다. 이는 구성 옵션을 전달하거나, 현재 모듈의 구성 또는 다른 모듈 인스턴스 정보에 따라 import된 모듈을 설정하기에 좋은 위치입니다. + + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }; +} +``` + +애플리케이션 레벨에서는 약간 다르게 동작합니다: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +루트 애플리케이션 모듈이 일반 모듈에서 생성된 경우, 일반 모듈과 유사하게 동작합니다. + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## 모듈 이름 + +모든 구성 옵션은 환경 변수를 통해서도 변경할 수 있습니다. 이는 모듈에 이름이 할당된 경우에만 동작합니다. 모듈 이름은 `createModule`을 통해 정의할 수 있으며, 인스턴스 생성 시점에 동적으로 변경할 수도 있습니다. 후자의 패턴은 동일한 모듈을 두 번 import하고 각각을 구분하고자 할 때 유용합니다. + +```typescript +export class MyModule extends createModuleClass({ + name: 'my' +}) { +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.name = 'my'; + }; +} +``` + +```typescript +import { MyModule } from './module'; + +new App({ + imports: [ + new MyModule(), //'my'가 기본 이름 + new MyModule().rename('my2'), //'my2'가 새로운 이름 + ] +}).run(); +``` + +환경 변수 또는 .env 파일에서 구성 옵션을 로드하는 방법에 대한 자세한 내용은 [구성](./configuration.md) 장을 참고하세요. + +## Imports + +모듈은 다른 모듈을 import하여 기능을 확장할 수 있습니다. `App`에서는 모듈 정의 객체의 `imports: []`를 통해 다른 모듈을 import할 수 있습니다: + +```typescript +new App({ + imports: [new Module] +}).run(); +``` + +일반 모듈에서는 객체 정의에 인스턴스를 넣으면 전역이 되어버리므로(보통 원치 않는 동작) 이 방식이 불가능합니다. 대신, 모듈 자체의 `imports` 프로퍼티를 통해 모듈 내에서 인스턴스를 생성하여, 모듈의 각 새 인스턴스마다 import된 모듈의 인스턴스가 생성되도록 할 수 있습니다. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + imports = [new OtherModule()]; +} + +export function myModule() { + return (module: AppModule) => { + module.addImport(new OtherModule()); + }; +} +``` + +`process` 훅을 사용하여 구성에 따라 모듈을 동적으로 import할 수도 있습니다. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + process() { + if (this.config.xEnabled) { + this.addImport(new OtherModule({ option: 'value' }); + } + } +} + +export function myModule(option: { xEnabled?: boolean } = {}) { + return (module: AppModule) => { + if (option.xEnabled) { + module.addImport(new OtherModule()); + } + }; +} +``` + +## 훅(Hooks) + +서비스 컨테이너는 루트/애플리케이션 모듈부터 시작하여 import된 순서대로 모든 모듈을 로드합니다. + +이 과정에서 서비스 컨테이너는 등록된 모든 구성 로더를 실행하고, `setupConfig` 콜백을 호출하며, 각 모듈의 구성 객체를 검증합니다. + +서비스 컨테이너 로딩의 전체 과정은 다음과 같습니다: + +1. 각 모듈 `T`에 대해(루트부터 시작) + 1. 구성 로더 `ConfigLoader.load(T)` 실행. + 2. `T.setupConfig()` 호출. + 3. `T`의 구성 검증. 유효하지 않으면 중단. + 4. `T.process()` 호출. + 여기서 모듈은 유효한 구성 옵션에 따라 자신을 수정할 수 있습니다. 새로운 import, provider 등을 추가합니다. + 5. `T`의 각 import된 모듈에 대해 1. 반복. +3. 등록된 모든 모듈 검색. +4. 발견된 각 모듈 `T`를 처리. + 1. `T`의 미들웨어 등록. + 2. 이벤트 디스패처에 `T`의 리스너 등록. + 3. 2.에서 발견된 모든 모듈에 대해 `Module.processController(T, controller)` 호출. + 4. 2.에서 발견된 모든 모듈에 대해 `Module.processProvider(T, token, provider)` 호출. + 5. `T`의 각 import된 모듈에 대해 3. 반복. +5. 모든 모듈에 대해 `T.postProcess()` 실행. +6. 모든 모듈에서 bootstrap 클래스 인스턴스화. +7. 의존성 주입 컨테이너가 이제 빌드됨. + +훅을 사용하려면 모듈 클래스에서 `process`, `processProvider`, `postProcess` 메서드를 등록하면 됩니다. + +```typescript +import { createModuleClass, AppModule } from '@deepkit/app'; +import { isClass } from '@deepkit/core'; +import { ProviderWithScope, Token } from '@deepkit/injector'; + +export class MyModule extends createModuleClass({}) { + imports = [new FrameworkModule()]; + + //먼저 실행됨 + process() { + //this.config에는 완전히 검증된 구성 객체가 들어 있습니다. + if (this.config.environment === 'development') { + this.getImportedModuleByClass(FrameworkModule).configure({ debug: true }); + } + this.addModule(new AnotherModule); + this.addProvider(Service); + + //추가 설정 메서드를 호출합니다. + //이 경우 의존성 주입 컨테이너가 Service를 인스턴스화할 때 + //'method1'을 주어진 인자로 호출합니다. + this.configureProvider<Service>(v => v.method1(this.config.value)); + } + + //모든 모듈에서 발견된 각 컨트롤러에 대해 실행됨 + processController(module: AppModule<any>, controller: ClassType) { + //예를 들어 HttpModule은 각 컨트롤러에 대해 @http 데코레이터가 사용되었는지 확인하고, + //그렇다면 모든 라우트 정보를 추출하여 라우터에 넣습니다. + } + + //모든 모듈에서 발견된 각 provider에 대해 실행됨 + processProvider(module: AppModule<any>, token: Token, provider: ProviderWithScope) { + //예를 들어 FrameworkModule은 deepkit/orm Database를 상속하는 제공된 토큰을 찾아 + //마이그레이션 CLI 명령과 Framework Debugger에서 사용할 수 있도록 + //자동으로 DatabaseRegistry에 등록합니다. + } + + //모든 모듈이 처리된 후 실행됨. + //process/processProvider에서 처리한 정보를 기반으로 + //module.configureProvider를 통해 provider를 설정할 수 있는 마지막 기회입니다. + postProcess() { + + } +} +``` + +## 상태를 가진 모듈 + +각 모듈은 `new Module`로 명시적으로 인스턴스화되기 때문에 상태를 가질 수 있습니다. 이 상태는 의존성 주입 컨테이너에 주입되어 서비스에서 사용할 수 있습니다. + +예시로 HttpModule의 사용 사례를 생각해봅시다. 애플리케이션 전체에 등록된 각 컨트롤러에 특정 @http 데코레이터가 있는지 확인하고, 있다면 컨트롤러를 레지스트리에 넣습니다. 이 레지스트리는 Router에 주입되고, Router가 인스턴스화될 때 해당 컨트롤러의 모든 라우트 정보를 추출하여 등록합니다. + +```typescript +class Registry { + protected controllers: { module: AppModule<any>, classType: ClassType }[] = []; + + register(module: AppModule<any>, controller: ClassType) { + this.controllers.push({ module, classType: controller }); + } + + get(classType: ClassType) { + const controller = this.controllers.find(v => v.classType === classType); + if (!controller) throw new Error('Controller unknown'); + return controller; + } +} + +class Router { + constructor( + protected injectorContext: InjectorContext, + protected registry: Registry + ) { + } + + getController(classType: ClassType) { + //주어진 컨트롤러 classType에 대한 classType과 모듈을 찾습니다 + const controller = this.registry.get(classType); + + //여기서 컨트롤러가 인스턴스화됩니다. 이미 인스턴스화되어 있으면 + //이전 인스턴스를 반환합니다(해당 provider가 transient: true가 아닌 경우) + return injector.get(controller.classType, controller.module); + } +} + +class HttpModule extends createModuleClass({ + providers: [Router], + exports: [Router], +}) { + protected registry = new Registry; + + process() { + this.addProvider({ provide: Registry, useValue: this.registry }); + } + + processController(module: AppModule<any>, controller: ClassType) { + //컨트롤러 소비자는 컨트롤러를 모듈의 providers에 넣어야 합니다 + if (!module.isProvided(controller)) module.addProvider(controller); + this.registry.register(module, controller); + } +} + +class MyController {} + +const app = new App({ + controllers: [MyController], + imports: [new HttpModule()] +}); + +const myController = app.get(Router).getController(MyController); +``` + +## 루트용 + +`root` 프로퍼티를 사용하면 모듈의 의존성 주입 컨테이너를 루트 애플리케이션의 컨테이너로 이동시킬 수 있습니다. 이를 통해 모듈의 모든 서비스가 자동으로 루트 애플리케이션에서도 사용할 수 있게 됩니다. 이는 기본적으로 각 provider(컨트롤러, 이벤트 리스너, provider)를 루트 컨테이너로 이동시키는 것입니다. 의존성 충돌이 발생할 수 있으므로, 정말 전역만 포함하는 모듈에만 사용해야 합니다. 가능하면 각 provider를 수동으로 export하는 것을 권장합니다. + +여러 모듈에서 사용할 수 있는 라이브러리를 구축하는 경우, `root` 사용을 피해야 합니다. 다른 라이브러리의 provider 토큰과 충돌할 수 있기 때문입니다. 예를 들어, 이 라이브러리 모듈이 서비스를 정의하는 `foo` 모듈을 import하고, 일부 서비스를 필요에 맞게 재구성했는데, 사용자의 애플리케이션도 동일한 `foo` 모듈을 import하는 경우, 사용자는 당신이 재구성한 서비스를 받게 됩니다. 더 단순한 사용 사례에서는 문제가 되지 않을 수도 있습니다. + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + root = true; +} +``` + +서드파티 모듈의 `root` 프로퍼티를 `forRoot()`를 사용하여 변경할 수도 있습니다. + +```typescript +new App({ + imports: [new ThirdPartyModule().forRoot()], +}).run(); +``` + +## Injector Context + +InjectorContext는 의존성 주입 컨테이너입니다. 이를 사용하여 자신의 모듈이나 다른 모듈에서 서비스의 요청/인스턴스화를 수행할 수 있습니다. 예를 들어 `processControllers`에서 컨트롤러를 저장해 두었다가, 이를 올바르게 인스턴스화하고자 할 때 필요합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/app/services.md b/website/src/translations/ko/documentation/app/services.md new file mode 100644 index 000000000..365f1bae5 --- /dev/null +++ b/website/src/translations/ko/documentation/app/services.md @@ -0,0 +1,62 @@ +# 서비스 + + +서비스는 애플리케이션에 필요한 모든 값, 함수, 또는 기능을 포괄하는 넓은 범주의 개념입니다. 서비스는 보통 범위가 좁고 명확히 정의된 목적을 가진 클래스입니다. 특정한 일을 잘 수행하도록 해야 합니다. + +Deepkit(및 대부분의 다른 JavaScript/TypeScript 프레임워크)에서 서비스는 프로바이더를 사용해 모듈에 등록된 단순한 클래스입니다. 가장 단순한 프로바이더는 클래스 프로바이더로, 클래스 자체만 지정하며 그 외에는 아무것도 지정하지 않습니다. 이렇게 하면 서비스는 정의된 모듈의 의존성 주입 컨테이너에서 싱글톤이 됩니다. + +서비스는 의존성 주입 컨테이너에 의해 처리되고 인스턴스화되므로, 생성자 주입 또는 프로퍼티 주입을 사용해 다른 서비스, 컨트롤러, 이벤트 리스너에서 가져와 사용할 수 있습니다. 자세한 내용은 [의존성 주입](../dependency-injection) 장을 참조하세요. + +간단한 서비스를 만들기 위해 목적을 가진 클래스를 작성합니다: + + +```typescript +export interface User { + username: string; +} + +export class UserManager { + users: User[] = []; + + addUser(user: User) { + this.users.push(user); + } +} +``` + +그리고 애플리케이션이나 모듈에 등록합니다: + +```typescript +new App({ + providers: [UserManager] +}).run(); +``` + +그 후 이 서비스를 컨트롤러, 다른 서비스, 또는 이벤트 리스너에서 사용할 수 있습니다. 예를 들어, 이 서비스를 CLI 커맨드나 HTTP 라우트에서 사용해 봅시다: + + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + providers: [UserManager], + imports: [new FrameworkModule({debug: true})] +}); + +app.command('test', (userManager: UserManager) => { + for (const user of userManager.users) { + console.log('User: ', user.username); + } +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', (userManager: UserManager) => { + return userManager.users; +}) + +app.run(); +``` + +서비스는 Deepkit의 근본적인 빌딩 블록이며 클래스에만 국한되지 않습니다. 실제로 서비스는 애플리케이션에 필요한 모든 값, 함수, 또는 기능이 될 수 있습니다. 더 자세히 알아보려면 [의존성 주입](../dependency-injection) 장을 참조하세요. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/broker.md b/website/src/translations/ko/documentation/broker.md new file mode 100644 index 000000000..4fb677a77 --- /dev/null +++ b/website/src/translations/ko/documentation/broker.md @@ -0,0 +1,155 @@ +# Deepkit Broker + +Deepkit Broker는 message queue, message bus, event bus, pub/sub, key/value store, cache, 그리고 원자적(atomic) 연산을 위한 고성능 추상화입니다. 자동 serialization과 validation, 타입 세이프티, 고성능, 그리고 확장성을 핵심으로 합니다. + +Deepkit Broker는 클라이언트이자 서버를 겸합니다. 독립 실행형 서버로도 사용할 수 있고, 다른 Deepkit Broker 서버에 연결하는 클라이언트로도 사용할 수 있습니다. 마이크로서비스 아키텍처에서 사용하도록 설계되었지만, 모놀리식(monolith) 애플리케이션에서도 사용할 수 있습니다. + +클라이언트는 다양한 백엔드를 지원하기 위해 adapter 패턴을 사용합니다. 동일한 코드를 서로 다른 백엔드에서 사용할 수 있고, 동시에 여러 백엔드를 사용할 수도 있습니다. + +현재 3개의 adapter가 제공됩니다. 하나는 기본 `BrokerDeepkitAdapter`로 Deepkit Broker 서버와 통신하며 Deepkit Broker에 기본 포함되어 있습니다(서버 포함). +두 번째는 Redis 서버와 통신하는 [@deepkit/broker-redis](./package/broker-redis)의 `BrokerRedisAdapter`이며, 세 번째는 테스트 목적의 인메모리 adapter인 `BrokerMemoryAdapter`입니다. + +## 설치 + +Deepkit Framework를 사용할 때는 Deepkit Broker가 기본으로 설치 및 활성화됩니다. 그렇지 않다면 다음으로 설치할 수 있습니다: + +```bash +npm install @deepkit/broker +``` + +## Broker Classes + +Deepkit Broker는 다음과 같은 주요 broker 클래스들을 제공합니다: + +- **BrokerCache** - 캐시 무효화(invalidation)를 지원하는 L2 캐시 추상화 +- **BrokerBus** - Message bus (Pub/Sub) +- **BrokerQueue** - Queue 시스템 +- **BrokerLock** - 분산 Lock +- **BrokerKeyValue** - Key/Value store + +이 클래스들은 broker 서버와 타입 세이프한 방식으로 통신하기 위해 broker adapter를 받도록 설계되었습니다. + +```typescript +import { BrokerBus, BrokerMemoryAdapter } from '@deepkit/broker'; + +const bus = new BrokerBus(new BrokerMemoryAdapter()); +const channel = bus.channel<{ foo: string }>('my-channel-name'); +await channel.subscribe((message) => { + console.log('received message', message); +}); + +await channel.publish({ foo: 'bar' }); +``` + +FrameworkModule은 기본 adapter를 제공하고 등록하며, 기본적으로 동일한 프로세스에서 실행되는 Deepkit Broker 서버에 연결합니다. + +런타임 타입과 `channel<Type>('my-channel-name');` 호출 덕분에 모든 것이 타입 세이프하며, 메시지는 adapter에서 자동으로 validation과 serialization을 수행할 수 있습니다. +기본 구현인 `BrokerDeepkitAdapter`가 이를 자동으로 처리합니다(BSON을 사용하여 serialization). + +각 broker 클래스는 자체 adapter interface를 가지고 있으므로, 필요한 Method만 구현할 수 있습니다. `BrokerDeepkitAdapter`는 이 모든 interface를 구현하며 모든 broker API에서 사용할 수 있습니다. + +## 애플리케이션 통합 + +의존성 주입과 함께 Deepkit 애플리케이션에서 이 클래스들을 사용하려면, 다음을 제공하는 `FrameworkModule`을 사용할 수 있습니다: + +- broker 서버를 위한 기본 adapter +- broker 서버 자체(자동으로 시작) +- 모든 broker 클래스를 provider로 등록 + +`FrameworkModule`은 주어진 설정을 기반으로 구성된 broker 서버에 대한 기본 broker adapter를 제공합니다. +또한 모든 broker 클래스에 대한 provider를 등록하므로, 서비스와 provider factory에 이들을 직접 주입할 수 있습니다(예: BrokerBus). + +```typescript +// 별도 파일에, 예: broker-channels.ts +type MyBusChannel = BrokerBusChannel<MyMessage>; + +const app = new App({ + providers: [ + Service, + provide<MyBusChannel>((bus: BrokerBus) => bus.channel<MyMessage>('my-channel-name')), + ], + imports: [new FrameworkModule({ + broker: { + // startOnBootstrap이 true이면 broker 서버가 이 주소에서 시작됩니다. Unix 소켓 경로 또는 host:port 조합 + listen: 'localhost:8811', // 또는 'var/broker.sock'; + // 다른 broker 서버를 사용하려면, 이 주소를 지정합니다. Unix 소켓 경로 또는 host:port 조합 + host: 'localhost:8811', // 또는 'var/broker.sock'; + // 메인 프로세스에서 단일 broker를 자동으로 시작합니다. 커스텀 broker 노드가 있다면 비활성화하세요. + startOnBootstrap: true, + }, + })], +}); +``` + +이후 서비스에 파생된 broker 클래스들(또는 broker 클래스 자체)을 주입할 수 있습니다: + +```typescript +import { MyBusChannel } from './broker-channels.ts'; + +class Service { + constructor(private bus: MyBusChannel) { + } + + async addUser() { + await this.bus.publish({ foo: 'bar' }); + } +} +``` + +채널을 위한 파생 타입(위의 `MyBusChannel`처럼)을 만들어 두면 서비스에 쉽게 주입할 수 있어 항상 좋습니다. +그렇지 않다면 `BrokerBus`를 주입하고 사용할 때마다 `channel<MyMessage>('my-channel-name')`를 호출해야 하므로, 오류가 발생하기 쉽고 DRY하지 않습니다. + +대부분의 broker 클래스는 이와 같은 방식의 파생을 지원하므로, 한 곳에서 정의해두고 어디서든 사용할 수 있습니다. 자세한 내용은 해당 broker 클래스 문서를 참고하세요. + +## 커스텀 Adapter + +커스텀 adapter가 필요하다면, `@deepkit/broker`의 다음 interface 중 하나 이상을 구현하여 자체 adapter를 만들 수 있습니다: + +```typescript +export type BrokerAdapter = BrokerAdapterCache & BrokerAdapterBus & BrokerAdapterLock & BrokerAdapterQueue & BrokerAdapterKeyValue; +``` + +```typescript +import { BrokerAdapterBus, BrokerBus, Release } from '@deepkit/broker'; +import { Type } from '@deepkit/type'; + +class MyAdapter implements BrokerAdapterBus { + disconnect(): Promise<void> { + // 구현: broker 서버와의 연결 해제 + } + async publish(name: string, message: any, type: Type): Promise<void> { + // 구현: broker 서버로 메시지 전송. name은 'my-channel-name', message는 { foo: 'bar' } + } + async subscribe(name: string, callback: (message: any) => void, type: Type): Promise<Release> { + // 구현: broker 서버 구독. name은 'my-channel-name' + } +} + +// 또는 기본 adapter인 BrokerDeepkitAdapter 사용 +const adapter = new MyAdapter; +const bus = new BrokerBus(adapter); + +``` + +## Broker 서버 + +`FrameworkModule`을 임포트하고 `server:start` 명령을 실행하면 Broker 서버가 기본으로 자동 시작됩니다. +모든 Broker 클래스는 기본적으로 이 서버에 연결되도록 구성됩니다. + +프로덕션 환경에서는 broker 서버를 별도 프로세스나 다른 머신에서 실행하는 것이 일반적입니다. +`server:start` 대신 `server:broker:start`로 broker 서버를 시작합니다. + +트래픽이 많고 확장이 필요하다면, 대신 [redis adapter](./package/broker-redis.md)를 사용하는 것이 좋습니다. Redis 서버를 실행해야 하므로 설정이 더 복잡하지만, 성능이 더 좋고 더 많은 트래픽을 처리할 수 있습니다. + +```bash +ts-node app.ts server:broker:start +``` + +이는 예를 들어 `new FrameworkModule({broker: {listen: 'localhost:8811'}})`로 구성된 호스트에서 서버를 시작합니다. +`app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper'});`를 활성화했다면 환경 변수로도 주소를 변경할 수 있습니다: + +```bash +APP_FRAMEWORK_BROKER_LISTEN=localhost:8811 ts-node app.ts server:broker:start +``` + +서버를 수동으로 시작하는 경우, 애플리케이션 설정에서 `startOnBootstrap: false`로 자동 broker 서버 시작을 비활성화했는지 확인하세요. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/broker/atomic-locks.md b/website/src/translations/ko/documentation/broker/atomic-locks.md new file mode 100644 index 000000000..82a0a77ae --- /dev/null +++ b/website/src/translations/ko/documentation/broker/atomic-locks.md @@ -0,0 +1,75 @@ +# Broker 원자적 Lock + +Deepkit Broker Locks는 여러 프로세스나 머신 전반에서 원자적 lock을 생성하는 간단한 방법입니다. + +동시에 오직 하나의 프로세스만 특정 코드 블록을 실행하도록 보장하는 간단한 방법입니다. + +## 사용법 + +```typescript +import { BrokerLock } from '@deepkit/broker'; + +const lock = new BrokerLock(adapter); + +// lock은 60초 동안 유효합니다. +// 획득 timeout은 10초입니다. +const myLock = lock.item('my-lock', { ttl: '60s', timeout: '10s' }); + +async function criticalSection() { + // 함수가 끝날 때까지 lock을 유지합니다. + // 함수가 에러를 던져도 lock을 자동으로 정리합니다. + await using hold = await lock1.hold(); +} +``` + +이 lock은 [명시적 리소스 관리](https://github.com/tc39/proposal-explicit-resource-management)를 지원하므로, lock을 제대로 해제하기 위해 try-catch 블록을 사용할 필요가 없습니다. + +lock을 수동으로 획득하고 해제하려면 `acquire` 및 `release` 메서드를 사용할 수 있습니다. + +```typescript +// lock을 획득할 때까지 대기합니다. +// 또는 timeout에 도달하면 예외를 던집니다 +await myLock.acquire(); + +try { + // 중요한 작업 수행 +} finally { + await myLock.release(); +} +``` + +## 앱에서의 사용 + +애플리케이션에서 BrokerLock을 사용하는 전체 예제입니다. +`FrameworkModule`을 import하면 이 Class는 의존성 주입 컨테이너에서 자동으로 사용 가능합니다. +자세한 내용은 Getting started 페이지를 참고하세요. + +```typescript +import { BrokerLock, BrokerLockItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 이 Type을 공유 파일로 이동하세요 +type MyCriticalLock = BrokerLockItem; + +class Service { + constructor(private criticalLock: MyCriticalLock) { + } + + async doSomethingCritical() { + await using hold = await this.criticalLock.hold(); + + // 중요한 작업 수행, + // lock은 자동으로 해제됩니다 + } +} + +const app = new App({ + providers: [ + Service, + provide<MyCriticalLock>((lock: BrokerLock) => lock.item('my-critical-lock', { ttl: '60s', timeout: '10s' })), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/broker/cache.md b/website/src/translations/ko/documentation/broker/cache.md new file mode 100644 index 000000000..fd7975a94 --- /dev/null +++ b/website/src/translations/ko/documentation/broker/cache.md @@ -0,0 +1,89 @@ +# 브로커 캐시 + +Deepkit Broker Cache Class는 메모리에 휘발성 로컬 캐시를 유지하는 2단계(2 levels) 캐시로, 데이터가 캐시에 없거나 만료되었거나 무효화된 경우에만 브로커 서버에서 데이터를 가져옵니다. 이를 통해 매우 높은 성능과 낮은 지연으로 데이터를 가져올 수 있습니다. + +이 캐시는 type-safe하도록 설계되었으며 데이터는 자동으로 직렬화/역직렬화(BSON 사용)됩니다. 또한 캐시 무효화와 캐시 초기화를 지원합니다. 구현은 동일한 캐시 아이템에 동시에 여러 요청이 접근하더라도 프로세스당 캐시가 한 번만 재구성되도록 보장합니다. + +데이터는 서버에 영구 저장되지 않고 메모리에만 유지됩니다. 서버가 재시작되면 모든 데이터가 유실됩니다. + +## 사용법 + +BrokerCache가 의존성 주입 컨테이너에서 사용 가능하도록 애플리케이션을 올바르게 설정하는 방법은 시작하기 페이지를 확인하세요. + +Deepkit Broker의 캐시 추상화는 단순한 key/value store와 매우 다릅니다. 캐시 이름과 캐시가 비어 있거나 만료되었을 때 자동으로 호출되는 builder Function을 정의하는 방식으로 동작합니다. 이 builder Function은 캐시에 저장될 데이터를 생성하는 역할을 합니다. + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; + +const cache = new BrokerCache(adapter); + +const cacheItem = cache.item('my-cache', async () => { + // 이것은 캐시가 비어 있거나 만료되었을 때 호출되는 builder Function입니다 + return 'hello world'; +}); + + +// 캐시가 만료되었거나 비어 있는지 확인 +await cacheItem.exists(); + +// 캐시에서 데이터를 가져오거나 브로커 서버에서 가져옵니다 +// 캐시가 비어 있거나 만료된 경우 builder Function이 호출되고 +// 결과가 반환되며 브로커 서버로 전송됩니다. +const topUsers = await cacheItem.get(); + +// 캐시를 무효화하여 다음 get() 호출 시 +// 다시 builder Function이 호출되도록 합니다. +// 로컬 캐시와 서버 캐시를 모두 지웁니다. +await cacheItem.invalidate(); + +// 캐시에 데이터를 수동으로 설정 +await cacheItem.set(xy); +``` + +## 앱에서의 사용 + +애플리케이션에서 BrokerCache를 사용하는 전체 예제입니다. +`FrameworkModule`을 임포트하면 해당 Class는 의존성 주입 컨테이너에서 자동으로 사용 가능합니다. +자세한 내용은 시작하기 페이지를 참고하세요. + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 이러한 타입을 공통 파일에 정의해 두면 재사용하기 좋고 +// 서비스에 주입하기도 용이합니다 +type MyCacheItem = BrokerCacheItem<User[]>; + +function createMyCache(cache: BrokerCache, database: Database) { + return cache.item<User[]>('top-users', async () => { + // 이것은 캐시가 비어 있거나 만료되었을 때 호출되는 builder Function입니다 + return await database.query(User) + .limit(10).orderBy('score').find(); + }); +} + +class Service { + constructor(private cacheItem: MyCacheItem) { + } + + async getTopUsers() { + return await this.cacheItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + Database, + provide<MyCacheItem>(createMyCache), + ], + imports: [ + new FrameworkModule(), + ], +}); + +const cacheItem = app.get<MyCacheItem>(); + +// 캐시에서 데이터를 가져오거나 브로커 서버에서 가져옵니다 +const topUsers = await cacheItem.get(); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/broker/key-value.md b/website/src/translations/ko/documentation/broker/key-value.md new file mode 100644 index 000000000..eb163b500 --- /dev/null +++ b/website/src/translations/ko/documentation/broker/key-value.md @@ -0,0 +1,91 @@ +# 브로커 Key-Value + +Deepkit Broker Key-Value Class는 broker server와 동작하는 간단한 key/value store 추상화입니다. broker server로부터 데이터를 저장하고 가져오는 간단한 방법입니다. + +로컬 캐싱은 구현되어 있지 않습니다. 모든 `get` 호출은 매번 브로커 서버로의 실제 네트워크 요청입니다. 이를 피하려면 Broker Cache 추상화를 사용하세요. + +데이터는 서버에 영구 저장되지 않고 메모리에만 유지됩니다. 서버가 재시작되면 모든 데이터가 사라집니다. + +## 사용법 + +```typescript +import { BrokerKeyValue } from '@deepkit/broker'; + +const keyValue = new BrokerKeyValue(adapter, { + ttl: '60s', // 각 key의 time-to-live. 0은 ttl 없음(기본값). +}); + +const item = keyValue.item<number>('key1'); + +await item.set(123); +console.log(await item.get()); //123 + +await item.remove(); +``` + +데이터는 지정된 Type을 기반으로 BSON을 사용해 자동으로 직렬화/역직렬화됩니다. + +`set`과 `get` 메서드는 `BrokerKeyValue` 인스턴스에서 직접 호출할 수도 있지만, +매번 key와 Type을 넘겨야 한다는 단점이 있습니다. + +```typescript +await keyValue.set<number>('key1', 123); +console.log(await keyValue.get<number>('key1')); //123 +``` + +## Increment + +`increment` Method는 주어진 값만큼 key의 값을 원자적으로 증가시킬 수 있게 해줍니다. + +이는 서버에 자체 저장 엔트리를 생성하며 `set` 또는 `get`과 호환되지 않는다는 점에 유의하세요. + +```typescript + +const activeUsers = keyValue.item<number>('activeUsers'); + +// 1씩 원자적으로 증가 +await activeUsers.increment(1); + +await activeUsers.increment(-1); + +// 현재 값을 얻는 유일한 방법은 0으로 increment를 호출하는 것입니다 +const current = await activeUsers.increment(0); + +// 엔트리를 제거합니다 +await activeUsers.remove(); +``` + +## 앱 사용법 + +애플리케이션에서 BrokerKeyValue를 사용하는 전체 예제입니다. +`FrameworkModule`을 import하면 이 Class는 DI 컨테이너에 자동으로 제공됩니다. +자세한 내용은 Getting Started 페이지를 참조하세요. + +```typescript +import { BrokerKeyValue, BrokerKeyValueItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 이 Type을 공유 파일로 이동하세요 +type MyKeyValueItem = BrokerKeyValueItem<User[]>; + +class Service { + constructor(private keyValueItem: MyKeyValueItem) { + } + + async getTopUsers(): Promise<User[]> { + // undefined일 수 있습니다. 이 경우를 처리해야 합니다. + // 이를 피하려면 Broker Cache를 사용하세요. + return await this.keyValueItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + provide<MyKeyValueItem>((keyValue: BrokerKeyValue) => keyValue.item<User[]>('top-users')), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/broker/message-bus.md b/website/src/translations/ko/documentation/broker/message-bus.md new file mode 100644 index 000000000..0f1796ab6 --- /dev/null +++ b/website/src/translations/ko/documentation/broker/message-bus.md @@ -0,0 +1,137 @@ +# Broker Bus + +Deepkit Message Bus는 애플리케이션의 서로 다른 부분 간에 메시지나 이벤트를 전송할 수 있게 해주는 메시지 버스 시스템(Pub/Sub, 분산 이벤트 시스템)입니다. + +마이크로서비스, 모놀리식, 혹은 그 밖의 어떤 형태의 애플리케이션에서도 사용할 수 있습니다. 이벤트 주도 아키텍처에 완벽히 적합합니다. + +이는 프로세스 내 이벤트에 사용하는 Deepkit Event system과는 다릅니다. Broker Bus는 다른 프로세스나 서버로 보내야 하는 이벤트에 사용됩니다. 또한 FrameworkModule이 자동으로 시작한 여러 worker 간 통신이 필요할 때에도 Broker Bus는 매우 적합합니다(예: `new FrameworkModule({workers: 4})`). + +이 시스템은 타입 안전성을 염두에 두고 설계되었으며 메시지를 자동으로 직렬화/역직렬화합니다(BSON 사용). 메시지 타입에 [검증](../runtime-types/validation.md)을 추가하면, 전송 전과 수신 후에 메시지를 검증합니다. 이를 통해 메시지가 항상 올바른 형식을 유지하고 기대한 데이터를 포함함을 보장합니다. + +## 사용법 + +```typescript +import { BrokerBus } from '@deepkit/broker'; + +const bus = new BrokerBus(adapter); + +// move this type to a shared file +type UserEvent = { type: 'user-created', id: number } | { type: 'user-deleted', id: number }; + +const channel = bus.channel<Events>('user-events'); + +await channel.subscribe((event) => { + if (event.type === 'user-created') { + console.log('User created', event.id); + } else if (event.type === 'user-deleted') { + console.log('User deleted', event.id); + } +}); + +await channel.publish({ type: 'user-created', id: 1 }); +``` + +채널에 이름과 타입을 정의하면 올바른 타입의 메시지만 전송되고 수신되도록 보장할 수 있습니다. +데이터는 자동으로 직렬화/역직렬화됩니다(BSON 사용). + +## 앱에서의 사용 + +애플리케이션에서 BrokerBus를 사용하는 전체 예제입니다. +FrameworkModule을 가져오면 이 클래스는 의존성 주입 컨테이너에서 자동으로 사용할 수 있게 됩니다. +자세한 내용은 시작하기 페이지를 참고하세요. + +### Subject + +메시지를 전송하고 수신하는 기본 접근 방식은 rxjs `Subject` 타입을 사용하는 것입니다. 그 `subscribe` 및 `next` 메서드는 타입 안전한 방식으로 메시지를 보내고 받도록 해줍니다. 모든 Subject 인스턴스는 브로커에 의해 관리되며, Subject가 가비지 컬렉션되면 브로커 백엔드(예: Redis)에서 구독이 제거됩니다. + +메시지 발행 또는 구독 실패를 처리하려면 BrokerBus의 `BusBrokerErrorHandler`를 오버라이드하세요. + +이 접근 방식은 비즈니스 코드와 브로커 서버를 깔끔하게 분리해 주며, 브로커 서버 없이도 테스트 환경에서 동일한 코드를 사용할 수 있게 합니다. + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusSubject } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; +import { Subject } from 'rxjs'; + +// 이 타입은 공유 파일로 이동하세요 +type MyChannel = Subject<{ + id: number; + name: string; +}>; + +class Service { + // MyChannel은 singleton이 아니며, 요청마다 새 인스턴스가 생성됩니다. + // 그 수명은 프레임워크가 모니터링하며, Subject가 가비지 컬렉션되면 + // 브로커 백엔드(예: Redis)에서 구독이 제거됩니다. + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }); + } + + update() { + this.channel.next({ id: 1, name: 'Peter' }); + } +} + +@rpc.controller('my-controller') +class MyRpcController { + constructor(private channel: MyChannel) { + } + + @rpc.action() + getChannelData(): MyChannel { + return this.channel; + } +} + +const app = new App({ + controllers: [MyRpcController], + providers: [ + Service, + provideBusSubject<MyChannel>('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` + +### BusChannel + +메시지 전송에 대한 확인이 필요하고 각 경우마다 에러를 처리하고 싶다면 `BrokerBusChannel` 타입을 사용할 수 있습니다. `subscribe`와 `publish` 메서드는 Promise를 반환합니다. + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusChannel } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 이 타입은 공유 파일로 이동하세요 +type MyChannel = BrokerBusChannel<{ + id: number; + name: string; +}>; + +class Service { + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }).catch(e => { + console.error('Error while subscribing', e); + }); + } + + async update() { + await this.channel.publish({ id: 1, name: 'Peter' }); + } +} + +const app = new App({ + providers: [ + Service, + provideBusChannel<MyChannel>('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/broker/message-queue.md b/website/src/translations/ko/documentation/broker/message-queue.md new file mode 100644 index 000000000..8ae336467 --- /dev/null +++ b/website/src/translations/ko/documentation/broker/message-queue.md @@ -0,0 +1,116 @@ +# Broker Queue + +Deepkit Message Queue는 메시지를 큐 서버로 전송하고 워커가 이를 처리할 수 있게 해주는 메시지 큐 시스템입니다. + +이 시스템은 type-safe하도록 설계되어 있으며 메시지를 자동으로 직렬화/역직렬화합니다(BSON 사용). + +데이터는 서버에 영구 저장되므로 서버가 크래시하더라도 데이터가 유실되지 않습니다. + +## 사용법 + +```typescript +import { BrokerQueue, BrokerQueueChannel } from '@deepkit/broker'; + +const queue = new BrokerQueue(adapter); + +type User = { id: number, username: string }; + +const registrationChannel = queue.channel<User>('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +}); + +// 워커가 메시지를 소비합니다. +// 일반적으로 별도의 프로세스에서 수행됩니다. +await registrationChannel.consume(async (user) => { + console.log('User registered', user); + // 워커가 여기서 크래시하더라도 메시지는 유실되지 않습니다. + // 다른 워커에게 자동으로 다시 전달됩니다. + // 이 콜백이 에러 없이 반환되면, + // 메시지는 처리 완료로 표시되고 결국 제거됩니다. +}); + +// 메시지를 전송하는 애플리케이션 +await registrationChannel.produce({ id: 1, username: 'Peter' }); +``` + +## 앱 사용법 + +애플리케이션에서 BrokerQueue를 사용하는 전체 예시입니다. +`FrameworkModule`를 임포트하면 해당 Class는 의존성 주입 컨테이너에서 자동으로 사용 가능합니다. +자세한 내용은 Getting started 페이지를 참조하세요. + +큐 시스템을 최대한 활용하려면 메시지를 소비하는 여러 워커를 실행하는 것이 좋습니다. +http 라우트 등을 가진 메인 애플리케이션과는 별도의 `App`을 작성합니다. + +공통 Service는 공유 앱 모듈을 통해 공유합니다. Channel 정의는 애플리케이션 전반에서 공용 파일을 통해 공유합니다. + +```typescript +// file: channels.ts + +export type RegistrationChannel = BrokerQueueChannel<User>; +export const registrationChannelProvider = provide<RegistrationChannel>((queue: BrokerQueue) => queue.channel<User>('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +})); +``` + +```typescript +// file: worker.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +async function consumerCommand( + channel: RegistrationChannel, + database: Database) { + + await channel.consume(async (user) => { + // user로 무언가를 수행하고, + // 정보를 저장하거나 이메일을 보내는 등 작업을 수행합니다. + }); + + // broker에 대한 연결이 프로세스를 유지합니다. +} + +const app = new App({ + providers: [ + Database, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +app.command('consumer', consumerCommand); + +// 위의 worker 커맨드를 직접 시작합니다 +void app.run('consumer'); +``` + +그리고 애플리케이션에서는 다음과 같이 메시지를 생성(produce)합니다: + +```typescript +// file: app.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +class Service { + constructor(private channel: RegistrationChannel) { + } + + async registerUser(user: User) { + await this.channel.produce(user); + } +} + +const app = new App({ + providers: [ + Service, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +void app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/dependency-injection.md b/website/src/translations/ko/documentation/dependency-injection.md new file mode 100644 index 000000000..710c24972 --- /dev/null +++ b/website/src/translations/ko/documentation/dependency-injection.md @@ -0,0 +1,198 @@ +# 의존성 주입 (Dependency Injection) + +Dependency Injection (DI)는 Class와 Function이 자신의 의존성을 전달받아(receive) 사용하는 디자인 패턴입니다. 이는 Inversion of Control (IoC) 원칙을 따르며, 복잡한 코드를 더 잘 분리해 테스트 용이성, 모듈성, 명확성을 크게 향상시킵니다. Service Locator 패턴처럼 IoC 원칙을 적용하는 다른 디자인 패턴들도 있지만, 특히 엔터프라이즈 소프트웨어에서 DI가 지배적인 패턴으로 자리 잡았습니다. + +IoC 원칙을 설명하기 위해 다음 예시를 보겠습니다: + +```typescript +import { HttpClient } from 'http-library'; + +class UserRepository { + async getUsers(): Promise<Users> { + const client = new HttpClient(); + return await client.get('/users'); + } +} +``` + +UserRepository Class는 HttpClient를 의존성으로 갖습니다. 이 의존성 자체는 특별할 것이 없지만, `UserRepository`가 HttpClient를 스스로 생성한다는 점이 문제입니다. +HttpClient의 생성을 UserRepository 안에 캡슐화하는 것이 좋아 보일 수 있지만, 실제로는 그렇지 않습니다. 만약 HttpClient를 교체하고 싶다면 어떻게 할까요? 실제 HTTP 요청이 나가지 않도록 하면서 UserRepository를 단위 테스트하고 싶다면 어떻게 할까요? 이 Class가 HttpClient를 사용한다는 사실은 어떻게 알 수 있을까요? + +## 제어의 역전 (Inversion of Control) + +Inversion of Control (IoC)의 관점에서는 다음과 같이 HttpClient를 생성자가 가진 명시적 의존성으로 설정하는 대안(일명 constructor injection)을 사용합니다. + +```typescript +class UserRepository { + constructor( + private http: HttpClient + ) {} + + async getUsers(): Promise<Users> { + return await this.http.get('/users'); + } +} +``` + +이제 UserRepository는 더 이상 HttpClient를 생성하지 않고, UserRepository의 사용자가 생성합니다. 이것이 Inversion of Control (IoC)입니다. 제어가 반전(inverted)되거나 역전되었습니다. 구체적으로 이 코드는 의존성을 생성하거나 요청하지 않고 전달받아(injected) 사용하므로 Dependency Injection을 적용합니다. Dependency Injection은 IoC의 여러 변형 중 하나입니다. + +## Service Locator + +DI 외에도 Service Locator (SL)는 IoC 원칙을 적용하는 또 다른 방법입니다. 이는 의존성을 전달받는 것이 아니라 요청한다는 점에서 Dependency Injection의 상대 개념으로 흔히 간주됩니다. 위 코드에서 HttpClient를 다음과 같이 요청한다면 Service Locator 패턴이라고 합니다. + +```typescript +class UserRepository { + async getUsers(): Promise<Users> { + const client = locator.getHttpClient(); + return await client.get('/users'); + } +} +``` + +`locator.getHttpClient`라는 Function 이름은 무엇이든 될 수 있습니다. 대안으로는 `useContext(HttpClient)`, `getHttpClient()`, `await import("client")`와 같은 Function 호출이나 `container.get(HttpClient)` 또는 `container.http` 같은 컨테이너 쿼리가 있을 수 있습니다. 전역을 import하는 것은 약간 다른 형태의 service locator로, 모듈 시스템 자체를 locator로 사용합니다: + +```typescript +import { httpClient } from 'clients' + +class UserRepository { + async getUsers(): Promise<Users> { + return await httpClient.get('/users'); + } +} +``` + +이 모든 변형의 공통점은 HttpClient 의존성을 명시적으로 요청하며, 코드가 service container의 존재를 인지한다는 것입니다. 이는 코드를 프레임워크에 강하게 결합시키며, 코드를 깔끔하게 유지하려면 피하고자 하는 요소입니다. + +서비스 요청은 기본값으로 Property에만 일어나는 것이 아니라, 코드 중간에서도 발생할 수 있습니다. 코드 중간이라는 것은 type interface의 일부가 아니라는 뜻이므로, HttpClient의 사용이 숨겨집니다. HttpClient가 요청되는 방식에 따라, 다른 구현으로 교체하는 것이 때로는 매우 어렵거나 완전히 불가능할 수 있습니다. 특히 단위 테스트 영역과 명확성 측면에서 어려움이 발생하기 쉽기 때문에, service locator는 특정 상황에서 anti-pattern으로 분류되기도 합니다. + +## Dependency Injection + +Dependency Injection에서는 아무것도 요청하지 않고, 사용자에 의해 명시적으로 제공되거나 코드가 전달받습니다. Consumer는 어떤 service container에도 접근하지 않으며, `HttpClient`가 어떻게 생성되거나 가져와지는지 알지 못합니다. 핵심적으로 IoC 프레임워크로부터 코드가 decoupled되어 더 깔끔해집니다. + +필요한 것이 `HttpClient`라는 type임을 선언할 뿐입니다. Service Locator 대비 Dependency Injection의 핵심 차이점이자 장점은, Dependency Injection을 사용하는 코드는 어떠한 service container나 서비스 식별 시스템(서비스에 이름을 줄 필요가 없음) 없이도 완전히 잘 동작한다는 점입니다. IoC 프레임워크 컨텍스트 밖에서도 통용되는 단순한 type 선언일 뿐입니다. + +앞선 예시에서 보았듯이, 이미 dependency injection 패턴이 적용되어 있습니다. 구체적으로 constructor injection이 보이며, 의존성이 constructor에 선언되어 있습니다. 따라서 UserRepository는 이제 다음과 같이 인스턴스화해야 합니다. + +```typescript +const users = new UserRepository(new HttpClient()); +``` + +UserRepository를 사용하려는 코드는 모든 의존성을 제공(주입)해야 합니다. 매번 HttpClient를 새로 생성할지, 동일한 것을 매번 사용할지는 이제 Class의 사용자가 결정하며, 더 이상 Class 자체가 결정하지 않습니다. 이는 service locator의 경우처럼 요청하는 것도 아니고, 초기 예제처럼 스스로 완전히 생성하는 것도 아닙니다. 이러한 흐름의 역전은 다양한 장점이 있습니다: + +* 모든 의존성이 명시적으로 보이므로 코드를 이해하기가 더 쉽습니다. +* 모든 의존성이 독립적이어서, 필요 시 쉽게 대체할 수 있으므로 테스트가 더 쉽습니다. +* 의존성을 손쉽게 교체할 수 있으므로 코드가 더 모듈화됩니다. +* UserRepository가 더 이상 매우 복잡한 의존성을 스스로 생성할 책임을 지지 않으므로, Separation of Concern 원칙을 촉진합니다. + +하지만 명백한 단점도 바로 보입니다: 정말로 HttpClient 같은 모든 의존성을 내가 직접 생성하거나 관리해야 하나요? 예도 있고 아니오도 있습니다. 예, 스스로 의존성을 관리해도 전혀 문제가 없는 경우가 많습니다. 좋은 API의 특징은 의존성이 통제 불능 상태가 되지 않으며, 그럼에도 여전히 사용하기 편하다는 점입니다. 많은 애플리케이션이나 복잡한 라이브러리에서 충분히 그럴 수 있습니다. 많은 의존성을 가진 매우 복잡한 low-level API를 사용자에게 단순화하여 제공하기 위해서는 facade 패턴이 훌륭하게 적합합니다. + +## Dependency Injection 컨테이너 + +더 복잡한 애플리케이션에서는 모든 의존성을 직접 관리할 필요가 없습니다. 이를 위해 바로 dependency injection 컨테이너가 존재합니다. 이는 모든 객체를 자동으로 생성할 뿐만 아니라 의존성도 자동으로 "주입"하므로 수동으로 "new"를 호출할 필요가 없어집니다. constructor injection, method injection, property injection 등 여러 형태의 injection이 있습니다. 이를 통해 많은 의존성이 있는 복잡한 아키텍처도 쉽게 관리할 수 있습니다. + +dependency injection 컨테이너(DI 컨테이너 또는 IoC 컨테이너라고도 함)는 Deepkit에서 `@deepkit/injector`로 제공되며, Deepkit Framework에서는 App modules를 통해 이미 통합되어 있습니다. 위의 코드는 `@deepkit/injector` 패키지의 low-level API를 사용하면 다음과 같이 보입니다. + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders( + [UserRepository, HttpClient] +); + +const userRepo = injector.get(UserRepository); + +const users = await userRepo.getUsers(); +``` + +이 경우 `injector` 객체가 dependency injection 컨테이너입니다. "new UserRepository"를 사용하는 대신, 컨테이너는 `get(UserRepository)`를 사용해 UserRepository 인스턴스를 반환합니다. 컨테이너를 정적으로 초기화하기 위해 provider 목록을 `InjectorContext.forProviders` Function에 전달합니다(이 경우 단순히 Class들). +DI는 의존성을 제공하는 것에 관한 것이므로, 컨테이너에 의존성을 제공하며, 이 때문에 "provider"라는 기술 용어가 사용됩니다. + +Provider에는 여러 유형이 있습니다: ClassProvider, ValueProvider, ExistingProvider, FactoryProvider. 이들을 통해 DI 컨테이너로 매우 유연한 아키텍처를 구성할 수 있습니다. + +Provider 간의 모든 의존성은 자동으로 해석되며, `injector.get()` 호출이 발생하는 즉시 객체와 의존성이 생성되고 캐시되며, constructor argument(이는 constructor injection이라고 함)로 전달되거나, property로 설정(property injection이라고 함)되거나, method 호출에 전달(method injection이라고 함)됩니다. + +이제 HttpClient를 다른 것으로 교체하려면, HttpClient에 대해 다른 provider(여기서는 ValueProvider)를 정의할 수 있습니다: + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useValue: new AnotherHttpClient()}, +]); +``` + +`injector.get(UserRepository)`를 통해 UserRepository가 요청되는 즉시, AnotherHttpClient 객체를 받게 됩니다. 대안으로 여기서 ClassProvider를 사용하면, AnotherHttpClient의 모든 의존성도 DI 컨테이너에 의해 함께 관리할 수 있습니다. + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useClass: AnotherHttpClient}, +]); +``` + +모든 유형의 provider는 [의존성 주입 프로바이더](./dependency-injection/providers.md) 섹션에서 나열되고 설명되어 있습니다. + +여기서 주목할 점은 Deepkit의 DI 컨테이너는 Deepkit의 runtime types에서만 동작한다는 것입니다. 즉, Class, Type, Interface, Function을 포함하는 모든 코드는 런타임에서 type 정보를 사용할 수 있도록 Deepkit Type Compiler로 컴파일되어야 합니다. [런타임 타입](./runtime-types.md) 챕터를 참고하세요. + +## 의존성 역전 (Dependency Inversion) + +앞서의 UserRepository 예시는 UserRepository가 더 낮은 수준의 HTTP 라이브러리에 의존한다는 것을 보여줍니다. 또한 추상화(Interface) 대신 구체 구현(Class)을 의존성으로 선언하고 있습니다. 언뜻 보기에 객체지향 패러다임에 부합하는 것처럼 보이지만, 특히 복잡하고 큰 아키텍처에서는 문제를 야기할 수 있습니다. + +대안으로는 HttpClient 의존성을 추상화(Interface)로 전환하여, HTTP 라이브러리의 코드를 UserRepository로 import하지 않도록 하는 방법이 있습니다. + +```typescript +interface HttpClientInterface { + get(path: string): Promise<any>; +} + +class UserRepository { + concstructor( + private http: HttpClientInterface + ) {} + + async getUsers(): Promise<Users> { + return await this.http.get('/users'); + } +} +``` + +이를 의존성 역전 원칙이라고 합니다. UserRepository는 더 이상 HTTP 라이브러리 자체에 직접 의존하지 않고, 대신 추상화(Interface)에 기반합니다. 이로써 이 원칙의 두 가지 근본적인 목표를 달성합니다: + +* 상위 수준 모듈은 하위 수준 모듈에서 어떤 것도 import하지 않아야 합니다. +* 구현은 추상화(Interface)에 기반해야 합니다. + +두 구현(UserRepository와 HTTP 라이브러리)을 결합하는 작업은 이제 DI 컨테이너를 통해 수행할 수 있습니다. + +```typescript +import { InjectorContext } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); +``` + +Deepkit의 DI 컨테이너는 HttpClientInterface 같은 추상 의존성(Interface)을 해석할 수 있으므로, HttpClient가 HttpClientInterface를 구현했다면 UserRepository는 자동으로 HttpClient의 구현을 얻게 됩니다. + +이는 HttpClient가 명시적으로 HttpClientInterface를 구현(`class HttpClient implements HttpClientInterface`)하거나, HttpClient의 API가 단순히 HttpClientInterface와 호환되는 경우에 이루어집니다. + + +HttpClient가 API를 수정하여(예: `get` Method를 제거) 더 이상 HttpClientInterface와 호환되지 않으면, DI 컨테이너는 에러를 던집니다("the HttpClientInterface dependency was not provided"). 여기서 두 구현을 결합하려는 사용자는 해결책을 찾아야 합니다. 예를 들어, HttpClientInterface를 구현하고 Method 호출을 HttpClient로 올바르게 포워딩하는 adapter Class를 등록할 수 있습니다. + +대안으로, HttpClientInterface를 구체 구현으로 직접 제공할 수도 있습니다. + +```typescript +import { InjectorContext, provide } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository, HttpClientInterface } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + provide<HttpClientInterface>({useClass: HttpClient}), +]); +``` + +이론적으로 의존성 역전 원칙에는 장점이 있지만, 실무에서는 상당한 단점이 있음을 유의해야 합니다. 더 많은 Interface를 작성해야 하므로 코드가 늘어날 뿐만 아니라, 각 구현마다 각 의존성에 대한 Interface가 생기므로 복잡성도 증가합니다. 이 비용을 지불할 가치는 애플리케이션이 일정 규모에 도달하고 이러한 유연성이 필요할 때에만 충분합니다. 다른 모든 디자인 패턴 및 원칙과 마찬가지로, 이를 적용하기 전에 비용-효용을 충분히 고려해야 합니다. + +디자인 패턴은 가장 단순한 코드에까지 맹목적으로 일괄 적용해서는 안 됩니다. 그러나 복잡한 아키텍처, 대규모 애플리케이션, 또는 확장 중인 팀 등과 같은 전제가 갖춰진다면, 의존성 역전과 기타 디자인 패턴들이 비로소 진정한 힘을 발휘합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/dependency-injection/configuration.md b/website/src/translations/ko/documentation/dependency-injection/configuration.md new file mode 100644 index 000000000..a606d3e09 --- /dev/null +++ b/website/src/translations/ko/documentation/dependency-injection/configuration.md @@ -0,0 +1,92 @@ +# 설정 + +DI container는 설정 옵션의 주입도 허용합니다. 이러한 설정 주입은 constructor injection 또는 property injection을 통해 받을 수 있습니다. + +Module API는 configuration definition을 지원하며, 이는 일반적인 Class입니다. 이러한 Class에 Property를 제공하면 각 Property가 설정 옵션으로 동작합니다. TypeScript에서 Class를 정의하는 방식 덕분에, 각 Property마다 Type과 기본값을 정의할 수 있습니다. + +```typescript +class RootConfiguration { + domain: string = 'localhost'; + debug: boolean = false; +} + +const rootModule = new InjectorModule([UserRepository]) + .setConfigDefinition(RootConfiguration) + .addImport(lowLevelModule); +``` + +이제 `domain`과 `debug` 설정 옵션을 providers에서 type-safe하게 편리하게 사용할 수 있습니다. + +```typescript +class UserRepository { + constructor(private debug: RootConfiguration['debug']) {} + + getUsers() { + if (this.debug) console.debug('fetching users ...'); + } +} +``` + +옵션 값은 `configure()`를 통해 설정할 수 있습니다. + +```typescript + rootModule.configure({debug: true}); +``` + +기본값이 없지만 여전히 필요한 옵션은 `!`를 사용해 표시할 수 있습니다. 이는 모듈 사용자에게 값을 제공하도록 강제하며, 그렇지 않으면 Error가 발생합니다. + +```typescript +class RootConfiguration { + domain!: string; +} +``` + +## 검증 + +또한 이전 장들인 [검증](../runtime-types/validation.md)과 [직렬화](../runtime-types/serialization.md)에 나온 모든 serialization 및 validation Type들을 사용하여, 옵션이 가져야 하는 Type과 내용 제약을 매우 상세하게 지정할 수 있습니다. + +```typescript +class RootConfiguration { + domain!: string & MinLength<4>; +} +``` + +## 주입 + +설정 옵션은 다른 의존성과 마찬가지로 앞서 본 것처럼 DI container를 통해 안전하고 쉽게 주입할 수 있습니다. 가장 간단한 방법은 index access operator를 사용해 단일 옵션을 참조하는 것입니다: + +```typescript +class WebsiteController { + constructor(private debug: RootConfiguration['debug']) {} + + home() { + if (this.debug) console.debug('visit home page'); + } +} +``` + +설정 옵션은 개별적으로뿐만 아니라 그룹으로도 참조할 수 있습니다. 이를 위해 TypeScript utility Type `Partial`을 사용할 수 있습니다: + +```typescript +class WebsiteController { + constructor(private options: Partial<RootConfiguration, 'debug' | 'domain'>) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +모든 설정 옵션을 얻기 위해 configuration Class 자체를 직접 참조할 수도 있습니다: + +```typescript +class WebsiteController { + constructor(private options: RootConfiguration) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +다만 실제로 사용하는 설정 옵션만 참조하는 것을 권장합니다. 이는 단위 테스트를 단순화할 뿐만 아니라, 코드에서 실제로 무엇이 필요한지 파악하기도 쉬워집니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/dependency-injection/getting-started.md b/website/src/translations/ko/documentation/dependency-injection/getting-started.md new file mode 100644 index 000000000..c77a31a78 --- /dev/null +++ b/website/src/translations/ko/documentation/dependency-injection/getting-started.md @@ -0,0 +1,110 @@ +# 시작하기 + +Deepkit의 Dependency Injection은 Runtime Types에 기반하므로, Runtime Types가 이미 올바르게 설치되어 있어야 합니다. [런타임 타입](../runtime-types/getting-started.md)을 참고하세요. + +이 작업이 완료되면, `@deepkit/injector`를 설치하거나, 이미 해당 라이브러리를 내부적으로 사용하는 Deepkit 프레임워크를 사용할 수 있습니다. + +```sh + npm install @deepkit/injector +``` + +라이브러리가 설치되면, 해당 API를 바로 사용할 수 있습니다. + + +## 사용법 + +Dependency Injection을 사용하는 방법은 세 가지가 있습니다. + +* Injector API (저수준) +* Module API +* App API (Deepkit 프레임워크) + +Deepkit 프레임워크 없이 `@deepkit/injector`를 사용하려면, 앞의 두 가지 방식을 권장합니다. + +### Injector API + +Injector API는 [Dependency Injection 소개](../dependency-injection)에서 이미 다루었습니다. 단일 Class `InjectorContext`로 하나의 DI 컨테이너를 생성하는 매우 간단한 사용 방식이 특징이며, 모듈이 없는 더 단순한 애플리케이션에 특히 적합합니다. + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); + +const repository = injector.get(UserRepository); +``` + +이 경우의 `injector` 객체는 Dependency Injection 컨테이너입니다. `InjectorContext.forProviders` Function은 Provider 배열을 받습니다. 어떤 값을 전달할 수 있는지는 [Dependency Injection Providers](dependency-injection.md#di-providers) 섹션을 참고하세요. + +### Module API + +더 복잡한 API로 `InjectorModule` Class가 있으며, Provider들을 서로 다른 모듈에 저장하여 모듈별로 캡슐화된 DI 컨테이너를 여러 개 만들 수 있습니다. 또한 모듈별로 configuration Class를 사용할 수 있어, 자동으로 검증된 configuration 값을 Provider에 쉽게 제공할 수 있습니다. 모듈들은 서로를 import하고, Provider를 export하여 계층을 구축하고 깔끔하게 분리된 아키텍처를 만들 수 있습니다. + +애플리케이션이 더 복잡하고 Deepkit 프레임워크를 사용하지 않는 경우 이 API를 사용하는 것이 좋습니다. + +```typescript +import { InjectorModule, InjectorContext } from '@deepkit/injector'; + +const lowLevelModule = new InjectorModule([HttpClient]) + .addExport(HttpClient); + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +const injector = new InjectorContext(rootModule); +``` + +이 경우의 `injector` 객체는 Dependency Injection 컨테이너입니다. Provider는 여러 모듈로 나눌 수 있고, 모듈 import를 통해 다양한 위치에서 다시 가져올 수 있습니다. 이는 애플리케이션 또는 아키텍처의 계층을 반영하는 자연스러운 계층 구조를 만듭니다. +InjectorContext에는 항상 계층의 최상위 모듈(루트 모듈 또는 앱 모듈)을 전달해야 합니다. 이후 InjectorContext는 중개 역할만 하며, `injector.get()` 호출은 단순히 루트 모듈로 전달됩니다. 다만, 두 번째 인자로 모듈을 전달하면 루트가 아닌 모듈의 Provider도 가져올 수 있습니다. + +```typescript +const repository = injector.get(UserRepository); + +const httpClient = injector.get(HttpClient, lowLevelModule); +``` + +루트가 아닌 모든 모듈은 기본적으로 캡슐화되어, 해당 모듈의 모든 Provider는 그 모듈 내부에서만 사용할 수 있습니다. 다른 모듈에서도 사용하려면 해당 Provider를 export해야 합니다. export하면 Provider는 계층의 상위 모듈로 이동하며, 그 방식으로 사용할 수 있습니다. + +모든 Provider를 기본적으로 최상위인 루트 모듈로 export하려면 `forRoot` 옵션을 사용할 수 있습니다. 이렇게 하면 모든 Provider를 다른 모든 모듈에서 사용할 수 있습니다. + +```typescript +const lowLevelModule = new InjectorModule([HttpClient]) + .forRoot(); //모든 Provider를 루트로 export +``` + +### App API + +Deepkit 프레임워크를 사용하면, 모듈은 `@deepkit/app` API로 정의합니다. 이는 Module API를 기반으로 하므로, 그 기능을 그대로 사용할 수 있습니다. 추가로 강력한 hook을 사용하고 configuration loader를 정의하여 더욱 동적인 아키텍처를 구성할 수 있습니다. + +[프레임워크 모듈](../app/modules.md) 장에서 더 자세히 설명합니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry, HttpBody } from '@deepkit/http'; + +interface User { + username: string; +} + +class Service { + users: User[] = []; +} + +const app = new App({ + providers: [Service], + imports: [new FrameworkModule()], +}); + +const router = app.get(HttpRouterRegistry); + +router.post('/users', (body: HttpBody<User>, service: Service) => { + service.users.push(body); +}); + +router.get('/users', (service: Service): Users => { + return service.users; +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/dependency-injection/injection.md b/website/src/translations/ko/documentation/dependency-injection/injection.md new file mode 100644 index 000000000..1656281d9 --- /dev/null +++ b/website/src/translations/ko/documentation/dependency-injection/injection.md @@ -0,0 +1,71 @@ +# Injection + +dependency가 주입되기 때문에 이를 Dependency Injection이라고 합니다. 주입은 사용자(수동) 또는 DI container(자동)에 의해 이루어집니다. + +## Constructor Injection + +대부분의 경우 constructor injection이 사용됩니다. 모든 dependency는 constructor arguments로 지정되며 DI container에 의해 자동으로 주입됩니다. + +```typescript +class MyService { + constructor(protected database: Database) { + } +} +``` + +Optional한 dependency는 그렇게 표시해야 하며, 그렇지 않으면 provider를 찾을 수 없을 때 Error가 발생할 수 있습니다. + +```typescript +class MyService { + constructor(protected database?: Database) { + } +} +``` + +## Property Injection + +constructor injection의 대안으로 property injection이 있습니다. 이는 보통 dependency가 optional이거나 constructor가 너무 복잡할 때 사용됩니다. 인스턴스가 생성되면(즉, constructor가 실행되면) properties는 자동으로 할당됩니다. + +```typescript +import { Inject } from '@deepkit/core'; + +class MyService { + // 필수 + protected database!: Inject<Database>; + + // 또는 선택 + protected database?: Inject<Database>; +} +``` + +## Parameter Injection + +여러 곳에서 callback function을 정의할 수 있습니다. 예를 들어 HTTP Routes나 CLI commands가 그렇습니다. 이 경우 dependencies를 parameters로 정의할 수 있습니다. +이는 DI container에 의해 자동으로 주입됩니다. + +```typescript +import { Database } from './db'; + +app.get('/', (database: Database) => { + //... +}); +``` + +## Injector Context + +dependency를 동적으로 resolve하고 싶다면 `InjectorContext`를 주입하고 이를 사용하여 dependencies를 가져올 수 있습니다. + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class MyService { + constructor(protected context: InjectorContext) { + } + + getDatabase(): Database { + return this.context.get(Database); + } +} +``` + +이는 [Dependency Injection Scopes](./scopes.md)와 함께 작업할 때 특히 유용합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/dependency-injection/providers.md b/website/src/translations/ko/documentation/dependency-injection/providers.md new file mode 100644 index 000000000..6fd7d9932 --- /dev/null +++ b/website/src/translations/ko/documentation/dependency-injection/providers.md @@ -0,0 +1,311 @@ +# Providers + +Dependency Injection 컨테이너에서 의존성을 제공하는 방법에는 여러 가지가 있습니다. 가장 단순한 방식은 클래스를 지정하는 것입니다. 이는 short ClassProvider라고도 합니다. + +```typescript +new App({ + providers: [UserRepository] +}); +``` + +이는 클래스만 지정되므로 특별한 Provider를 나타냅니다. 그 외의 모든 Provider는 객체 리터럴로 지정해야 합니다. + +기본적으로 모든 Provider는 singleton으로 표시되어, 어떤 시점에도 인스턴스가 하나만 존재합니다. Provider가 배치될 때마다 새 인스턴스를 생성하려면 `transient` 옵션을 사용할 수 있습니다. 이렇게 하면 클래스는 매번 새로 생성되거나 factory가 매번 실행됩니다. + +```typescript +new App({ + providers: [{ provide: UserRepository, transient: true }] +}); +``` + +## ClassProvider + +short ClassProvider 외에도 클래스 대신 객체 리터럴인 일반 ClassProvider가 있습니다. + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: UserRepository }] +}); +``` + +이는 다음 두 가지와 동일합니다. + +```typescript +new App({ + providers: [{ provide: UserRepository }] +}); + +new App({ + providers: [UserRepository] +}); +``` + +이를 사용하여 Provider를 다른 클래스로 교체할 수 있습니다. + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: OtherUserRepository }] +}); +``` + +이 예제에서 `OtherUserRepository` 클래스도 DI 컨테이너에서 관리되며, 해당 의존성은 모두 자동으로 해결됩니다. + +## ValueProvider + +정적 값을 이 Provider로 제공할 수 있습니다. + +```typescript +new App({ + providers: [{ provide: OtherUserRepository, useValue: new OtherUserRepository() }] +}); +``` + +의존성으로 제공될 수 있는 것이 클래스 인스턴스만은 아니므로, 어떤 값이든 `useValue`로 지정할 수 있습니다. symbol이나 primitive(string, number, boolean)도 Provider 토큰으로 사용할 수 있습니다. + +```typescript +new App({ + providers: [{ provide: 'domain', useValue: 'localhost' }] +}); +``` + +primitive Provider 토큰은 의존성으로서 Inject Type으로 선언해야 합니다. + +```typescript +import { Inject } from '@deepkit/core'; + +class EmailService { + constructor(public domain: Inject<string, 'domain'>) {} +} +``` + +inject 별칭과 primitive Provider 토큰의 조합은 런타임 타입 정보가 없는 패키지에서 의존성을 제공하는 데에도 사용할 수 있습니다. + +```typescript +import { Inject } from '@deepkit/core'; +import { Stripe } from 'stripe'; + +export type StripeService = Inject<Stripe, '_stripe'>; + +new App({ + providers: [{ provide: '_stripe', useValue: new Stripe }] +}); +``` + +그리고 사용자 측에서는 다음과 같이 선언합니다. + +```typescript +class PaymentService { + constructor(public stripe: StripeService) {} +} +``` + +## ExistingProvider + +이미 정의된 Provider로의 포워딩을 정의할 수 있습니다. + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useValue: new OtherUserRepository()}, + {provide: UserRepository, useExisting: OtherUserRepository} + ] +}); +``` + +## FactoryProvider + +함수를 사용하여 Provider에 대한 값을 제공할 수 있습니다. 이 함수는 매개변수를 포함할 수 있으며, 해당 매개변수는 DI 컨테이너에 의해 다시 제공됩니다. 따라서 다른 의존성이나 구성 옵션에 접근할 수 있습니다. + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: () => { + return new OtherUserRepository() + }}, + ] +}); + +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: (domain: RootConfiguration['domain']) => { + return new OtherUserRepository(domain); + }}, + ] +}); + +new App({ + providers: [ + Database, + {provide: OtherUserRepository, useFactory: (database: Database) => { + return new OtherUserRepository(database); + }}, + ] +}); +``` + +## InterfaceProvider + +클래스와 primitive 외에도, 추상화(Interface)도 제공할 수 있습니다. 이는 `provide` 함수를 통해 이루어지며, 제공할 값에 타입 정보가 없는 경우 특히 유용합니다. + +```typescript +import { provide } from '@deepkit/injector'; + +interface Connection { + write(data: Uint16Array): void; +} + +class Server { + constructor (public connection: Connection) {} +} + +class MyConnection { + write(data: Uint16Array): void {} +} + +new App({ + providers: [ + Server, + provide<Connection>(MyConnection) + ] +}); +``` + +여러 Provider가 Connection Interface를 구현한 경우, 마지막 Provider가 사용됩니다. + +provide()의 인자로는 다른 모든 Provider 유형이 가능합니다. + +```typescript +const myConnection = {write: (data: any) => undefined}; + +new App({ + providers: [ + provide<Connection>({ useValue: myConnection }) + ] +}); + +new App({ + providers: [ + provide<Connection>({ useFactory: () => myConnection }) + ] +}); +``` + +## Asynchronous Providers + +`@deepkit/injector`의 설계는 비동기 Dependency Injection 컨테이너와 함께 비동기 Provider를 사용하는 것을 배제합니다. 이는 Provider 요청 또한 비동기여야 하며, 그 결과 애플리케이션 전체가 최상위에서 비동기로 동작해야 하기 때문입니다. + +무언가를 비동기로 초기화해야 한다면, 애플리케이션 서버 bootstrap으로 이 초기화를 이동해야 합니다. 거기서는 이벤트가 비동기일 수 있습니다. 또는 초기화를 수동으로 트리거할 수도 있습니다. + +## Configure Providers + +구성 콜백을 사용하면 Provider의 결과를 조작할 수 있습니다. 이는 예를 들어 다른 의존성 주입 방식인 method injection을 사용할 때 유용합니다. + +이들은 module API 또는 app API에서만 사용할 수 있으며 모듈 상단에서 등록됩니다. + +```typescript +class UserRepository { + private db?: Database; + setDatabase(db: Database) { + this.db = db; + } +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider<UserRepository>(v => { + v.setDatabase(db); +}); +``` + +`configureProvider`는 콜백에서 첫 번째 Parameter인 `v`로 UserRepository 인스턴스를 전달받으며, 그 인스턴스의 Method를 호출할 수 있습니다. + +Method 호출 외에도 Property를 설정할 수 있습니다. + +```typescript +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider<UserRepository>(v => { + v.db = new Database(); +}); +``` + +모든 콜백은 큐에 배치되며, 정의된 순서대로 실행됩니다. + +큐의 호출은 Provider가 생성되자마자 Provider의 실제 결과에 대해 실행됩니다. 즉, ClassProvider의 경우 인스턴스가 생성되는 즉시 그 클래스 인스턴스에 적용되고, FactoryProvider의 경우 Factory의 결과에 적용되며, ValueProvider의 경우 해당 Provider에 적용됩니다. + +정적 값뿐 아니라 다른 Provider를 참조하기 위해, 콜백의 인자로 단지 정의하는 것만으로 임의의 의존성을 주입할 수 있습니다. 이러한 의존성이 Provider 스코프 내에서 알려져 있는지 확인하십시오. + +```typescript +class Database {} + +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository, Database]) +rootModule.configureProvider<UserRepository>((v, db: Database) => { + v.db = db; +}); +``` + +## Nominal types + +`configureProvider`에 전달된 타입은, 마지막 예제의 `UserRepository`와 같이, 구조적 타입 검사로 해석되는 것이 아니라 nominal types로 해석된다는 점에 유의하세요. 이는 예를 들어 동일한 구조지만 다른 정체성을 가진 두 클래스/Interface는 호환되지 않음을 의미합니다. `get<T>` 호출이나 의존성이 해결될 때도 마찬가지입니다. + +이는 구조적 타입 검사에 기반한 TypeScript의 타입 검사 방식과 다릅니다. 이 설계 결정은 우발적인 오구성(예: 모든 클래스와 구조적으로 호환되는 빈 클래스를 요청하는 경우)을 방지하고 코드를 더 견고하게 만들기 위해 이루어졌습니다. + +다음 예제에서 `User1`과 `User2` 클래스는 구조적으로는 호환되지만 nominal하게는 호환되지 않습니다. 즉, `User1`을 요청해도 `User2`가 해결되지 않으며 그 반대도 마찬가지입니다. + +```typescript + +class User1 { + name: string = ''; +} + +class User2 { + name: string = ''; +} + +new App({ + providers: [User1, User2] +}); +``` + +클래스를 상속하거나 Interface를 구현하면 nominal 관계가 성립합니다. + +```typescript +class UserBase { + name: string = ''; +} + +class User extends UserBase { +} + +const app = new App({ + providers: [User2] +}); + +app.get(UserBase); // User를 반환 +``` + +```typescript +interface UserInterface { + name: string; +} + +class User implements UserInterface { + name: string = ''; +} + +const app = new App({ + providers: [User] +}); + +app.get<UserInterface>(); // User를 반환 +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/dependency-injection/scopes.md b/website/src/translations/ko/documentation/dependency-injection/scopes.md new file mode 100644 index 000000000..2eade74e3 --- /dev/null +++ b/website/src/translations/ko/documentation/dependency-injection/scopes.md @@ -0,0 +1,54 @@ +# 스코프 + +기본적으로 DI container의 모든 provider는 singleton이며 따라서 한 번만 인스턴스화됩니다. 이는 UserRepository 예시에서 전체 런타임 동안 항상 하나의 UserRepository instance만 존재함을 의미합니다. 사용자가 "new" 키워드로 수동으로 생성하지 않는 한 어떤 시점에서도 두 번째 instance가 생성되지 않습니다. + +하지만 provider가 짧은 시간 동안만 또는 특정 이벤트 동안만 인스턴스화되어야 하는 다양한 사용 사례가 있습니다. 그러한 이벤트는 예를 들어 HTTP request나 RPC call일 수 있습니다. 이는 각 이벤트마다 새 instance가 생성되고, 더 이상 사용되지 않으면 자동으로 제거됨(garbage collector에 의해)을 의미합니다. + +HTTP request는 scope의 대표적인 예입니다. 예를 들어 session, user 객체 또는 기타 request 관련 provider를 이 scope에 등록할 수 있습니다. scope를 생성하려면 임의의 scope 이름을 선택한 다음 provider에 이를 지정하면 됩니다. + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class UserSession {} + +const injector = InjectorContext.forProviders([ + {provide: UserSession, scope: 'http'} +]); +``` + +한 번 scope가 지정되면 이 provider는 DI container에서 직접 획득할 수 없으므로 다음 호출은 실패합니다: + +```typescript +const session = injector.get(UserSession); //예외 발생 +``` + +대신 scoped DI container를 생성해야 합니다. 이는 매번 HTTP request가 들어올 때 발생합니다: + +```typescript +const httpScope = injector.createChildScope('http'); +``` + +이 scope에 등록된 provider뿐 아니라 scope를 정의하지 않은 모든 provider도 이제 이 scoped DI container에서 요청할 수 있습니다. + +```typescript +const session = httpScope.get(UserSession); //동작함 +``` + +모든 provider는 기본적으로 singleton이므로, `get(UserSession)`을 각 scoped container에서 호출할 때마다 항상 동일한 instance가 반환됩니다. 여러 scoped container를 생성하면 여러 개의 UserSession이 생성됩니다. + +scoped DI container는 외부에서 값을 동적으로 설정할 수 있습니다. 예를 들어 HTTP scope에서는 HttpRequest와 HttpResponse 객체를 쉽게 설정할 수 있습니다. + +```typescript +const injector = InjectorContext.forProviders([ + {provide: HttpResponse, scope: 'http'}, + {provide: HttpRequest, scope: 'http'}, +]); + +httpServer.on('request', (req, res) => { + const httpScope = injector.createChildScope('http'); + httpScope.set(HttpRequest, req); + httpScope.set(HttpResponse, res); +}); +``` + +Deepkit framework를 사용하는 애플리케이션에는 기본적으로 `http`, `rpc`, 그리고 `cli` scope가 있습니다. 각각 [CLI](../cli.md), [HTTP](../http.md), 또는 [RPC](../rpc.md) 장을 참고하세요. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem.md b/website/src/translations/ko/documentation/filesystem.md new file mode 100644 index 000000000..6c8a05318 --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem.md @@ -0,0 +1,273 @@ +# Deepkit Filesystem + +Deepkit Filesystem은 로컬 및 원격 파일시스템을 위한 파일시스템 추상화 계층입니다. 파일이 로컬에 저장되어 있든 원격 서버에 저장되어 있든 관계없이, 파일과 디렉터리를 일관된 방식으로 다룰 수 있습니다. + +기본적으로 지원되는 파일시스템: + +- 로컬 파일시스템 (패키지: `@deepkit/filesystem`) +- 메모리 (패키지: `@deepkit/filesystem`) +- FTP (패키지: `@deepkit/filesystem-ftp`) +- SFTP (패키지: `@deepkit/filesystem-sftp`) +- AWS S3 (패키지: `@deepkit/filesystem-aws-s3`) +- Google Cloud Filesystem (패키지: `@deepkit/filesystem-google`) + +## 설치 + +```bash +npm install @deepkit/filesystem +``` + +참고: NPM을 사용하지 않는 경우 peer dependencies가 올바르게 설치되어 있는지 확인하세요. + +## 사용법 + +```typescript +import { Filesystem, FilesystemLocalAdapter } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter('/path/to/my/files'); +const filesystem = new Filesystem(adapter); + +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} + +await filesystem.write('myFile.txt', 'Hello World'); +const file = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); + +const content = await filesystem.read('myFile.txt'); +``` + + +## 파일 목록 + +파일을 나열하려면 `files()` Method를 사용하세요. `File` 객체의 배열을 반환합니다. + +```typescript +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} +``` + +배열이 비어 있다면, 폴더가 없거나 해당 폴더에 파일이 없는 것입니다. + +모든 파일을 재귀적으로 나열하려면 `allFiles()` Method를 사용하세요. + +```typescript +const files = await filesystem.allFiles(); +for (const file of files) { + console.log(file.path); +} +``` + +## 파일 읽기 + +파일을 읽으려면 `read()` Method를 사용하세요. 파일 내용을 `Uint8Array`로 반환합니다. + +```typescript +const content = await filesystem.read('myFile.txt'); +``` + +텍스트로 읽으려면 `readAsText()` Method를 사용하세요. string을 반환합니다. + +```typescript +const content = await filesystem.readAsText('myFile.txt'); +``` + +## 파일 쓰기 + +파일을 쓰려면 `write()` Method를 사용하세요. `Uint8Array` 또는 string을 받습니다. + +```typescript +await filesystem.write('myFile.txt', 'Hello World'); +``` + +로컬 파일에서 쓰려면 `writeFile()` Method를 사용하세요. + +첫 번째 Argument는 파일 이름이 아닌 디렉터리입니다. 파일 이름은 제공된 파일의 basename입니다. +파일 이름을 변경하려면 두 번째 Argument로 {name: 'myFile.txt'} 객체를 전달하세요. 파일 확장자를 제공하지 않으면, +확장자는 자동으로 감지됩니다. + +```typescript +const path = await filesystem.writeFile('/', { path: '/path/to/local/file.txt' }); + +// UploadedFile 과 함께 동작합니다 +router.post('/upload', async (body: HttpBody<{ file: UploadedFile }>, filesystem: Filesystem, session: Session) => { + const user = session.getUser(); + const path = await filesystem.writeFile('/user-images', body.file, { name: `user-${user.id}` }); + //path = /user-images/user-123.jpg +}); +``` + + +## 파일 삭제 + +파일을 삭제하려면 `delete()` Method를 사용하세요. + +```typescript +await filesystem.delete('myFile.txt'); +``` + +루트 폴더의 `myFile.txt` 파일을 삭제합니다. 경로가 디렉터리인 경우 Error를 던집니다. + +## 디렉터리 삭제 + +디렉터리를 삭제하려면 `deleteDirectory()` Method를 사용하세요. 디렉터리와 그 안의 모든 파일 및 디렉터리를 삭제합니다. + +```typescript +await filesystem.deleteDirectory('myFolder'); +``` + +## 디렉터리 생성 + +디렉터리를 생성하려면 `createDirectory()` Method를 사용하세요. + +```typescript +await filesystem.makeDirectory('myFolder'); +``` + +## 파일 이동 + +파일 또는 디렉터리를 이동하려면 `move()` Method를 사용하세요. `File` 객체 또는 path를 받습니다. + +```typescript +await filesystem.move('myFile.txt', 'myFolder/myFile.txt'); +``` + +`myFolder` 디렉터리가 없으면 생성합니다. + +## 파일 복사 + +파일 또는 디렉터리를 복사하려면 `copy()` Method를 사용하세요. `File` 객체 또는 path를 받습니다. + +```typescript +await filesystem.copy('myFile.txt', 'myFolder/myFile.txt'); +``` + +`myFolder` 디렉터리가 없으면 생성합니다. + +## 파일 정보 + +파일 정보를 얻으려면 `get()` Method를 사용하세요. `File` 객체를 반환합니다. + +```typescript +const file: FilesystemFile = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); +``` + +path, 바이트 단위의 파일 크기, 마지막 수정 날짜를 반환합니다. Adapter가 지원하지 않으면 수정 날짜는 undefined일 수 있습니다. + +File 객체에는 다음과 같은 유용한 Method도 있습니다: + +```typescript +interface FilesystemFile { + path: string; + type: FileType; //파일 또는 디렉터리 + size: number; + lastModified?: Date; + /** + * 파일의 visibility. + * + * 일부 adapter는 파일의 visibility 읽기를 지원하지 않을 수 있습니다. + * 이 경우 visibility는 항상 'private'입니다. + * + * 일부 adapter는 파일 나열 시에는 visibility를 지원하지 않지만, 개별 파일 조회에서는 지원할 수 있습니다. + * 이 경우 visibility를 로드하려면 추가로 `filesystem.get(file)` 을 호출해야 합니다. + */ + visibility: FileVisibility; //public 또는 private + constructor(path: string, type?: FileType); + /** + * 이 파일이 심볼릭 링크인 경우 true를 반환합니다. + */ + isFile(): boolean; + /** + * 이 파일이 디렉터리인 경우 true를 반환합니다. + */ + isDirectory(): boolean; + /** + * 파일의 이름(basename)을 반환합니다. + */ + get name(): string; + /** + * 이 파일이 주어진 디렉터리에 있는 경우 true를 반환합니다. + */ + inDirectory(directory: string): boolean; + /** + * 파일의 디렉터리(dirname)를 반환합니다. + */ + get directory(): string; + /** + * 파일의 확장자를 반환합니다. 존재하지 않거나 디렉터리인 경우 빈 문자열을 반환합니다. + */ + get extension(): string; +} +``` + +## 파일 존재 여부 + +파일이 존재하는지 확인하려면 `exists()` Method를 사용하세요. + +```typescript + +if (await filesystem.exists('myFile.txt')) { + console.log('File exists'); +} +``` + +## 파일 Visibility + +Deepkit Filesystem은 파일을 public 또는 private으로 설정할 수 있는 단순한 file visibility 추상화를 지원합니다. + +예를 들어 S3나 Google Cloud Filesystem에서 유용합니다. 로컬 파일시스템의 경우 visibility에 따라 파일 권한을 설정합니다. + +파일의 visibility를 설정하려면 `setVisibility()` Method를 사용하거나, `write()`의 세 번째 Argument로 visibility를 전달하세요. + +```typescript +await filesystem.setVisibility('myFile.txt', 'public'); +await filesystem.write('myFile.txt', 'Hello World', 'public'); +``` + +파일의 visibility를 얻으려면 `getVisibility()` Method를 사용하거나 `FilesystemFile.visibility`를 확인하세요. + +```typescript +const visibility = await filesystem.getVisibility('myFile.txt'); +const file = await filesystem.get('myFile.txt'); +console.log(file.visibility); +``` + +일부 adapter는 `sotrage.files()`에서 visibility를 가져오는 것을 지원하지 않을 수 있습니다. 이 경우 visibility는 항상 `unknown`입니다. + +## 파일 공개 URL + +파일의 공개 URL을 얻으려면 `publicUrl()` Method를 사용하세요. 이는 파일이 public이고 adapter가 이를 지원하는 경우에만 작동합니다. + +```typescript +const url = filesystem.publicUrl('myFile.txt'); +``` + +adapter가 public url을 지원하지 않는 경우, Filesystem 추상화는 `option.baseUrl`을 사용하여 URL을 생성해 처리합니다. + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + baseUrl: 'https://my-domain.com/assets/' +}); + +const url = await filesystem.publicUrl('myFile.txt'); +console.log(url); //https://my-domain.com/assets/myFile.txt +``` + +## Filesystem 옵션 + +`Filesystem` 생성자는 옵션이 포함된 두 번째 Argument를 받습니다. + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + visibility: 'private', //파일의 기본 visibility + directoryVisibility: 'private', //디렉터리의 기본 visibility + pathNormalizer: (path: string) => path, //path를 정규화합니다. 기본적으로 `[^a-zA-Z0-9\.\-\_]` 를 `-` 로 대체합니다. + urlBuilder: (path: string) => path, //파일의 public URL을 생성합니다. 기본적으로 baseUrl + path를 반환합니다 + baseUrl: '', //public URL을 위한 base URL +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/app.md b/website/src/translations/ko/documentation/filesystem/app.md new file mode 100644 index 000000000..d4c4f6f26 --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/app.md @@ -0,0 +1,83 @@ +# 앱 + +Deepkit Storage는 단독으로도 동작하지만, 보통은 Dependency Injection을 사용하여 Deepkit App에서 이를 사용하고자 할 것입니다. + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +const app = new App({ + providers: [ + provideFilesystem(new FilesystemLocalAdapter({root: __dirname + '/public'})), + ] +}); + +app.command('write', async (content: string, fs: Filesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: Filesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + + +## 여러 Filesystem + +여러 Filesystem을 동시에 사용할 수 있습니다. 이를 위해 `provideNamedFilesystem('name', ...)`로 등록하고, `NamedFilesystem<'name'>`로 Filesystem 인스턴스를 주입받습니다. + +```typescript +import { App } from '@deepkit/app'; +import { NamedFilesystem, FilesystemLocalAdapter, provideNamedFilesystem } from '@deepkit/filesystem'; + +type PrivateFilesystem = NamedFilesystem<'private'>; +type PublicFilesystem = NamedFilesystem<'public'>; + +const app = new App({ + providers: [ + provideNamedFilesystem('private', new FilesystemLocalAdapter({root: '/tmp/dir1'})), + provideNamedFilesystem('public', new FilesystemLocalAdapter({root: '/tmp/dir2'})), + ] +}); + +app.command('write', async (content: string, fs: PublicFilesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: PublicFilesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + +## 구성 + +구성 옵션을 통해 adapter를 설정하는 것이 유용할 때가 많습니다. 예를 들어, local adapter의 루트 디렉터리나 AWS S3 또는 Google Storage adapter의 자격 증명을 설정하고 싶을 수 있습니다. + +이를 위해 [Configuration Injection](../app/configuration.md)을 사용할 수 있습니다. + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +class MyConfig { + fsRoot: string = '/tmp'; +} + +const app = new App({ + config: MyConfig, + providers: [ + provideFilesystem( + (config: MyConfig) => new FilesystemLocalAdapter({root: config.fsRoot}) + ), + ] +}); + +app.loadConfigFromEnv({prefix: 'APP_'}) +app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/aws-s3.md b/website/src/translations/ko/documentation/filesystem/aws-s3.md new file mode 100644 index 000000000..c9b284d08 --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/aws-s3.md @@ -0,0 +1,49 @@ +# AWS S3 파일시스템 + +AWS S3 filesystem adapter는 AWS S3 서비스를 Deepkit Filesystem으로 사용할 수 있게 합니다. + +이는 별도로 설치해야 하는 `@deepkit/filesystem-aws-s3`의 일부입니다. + +```sh +npm install @deepkit/filesystem-aws-s3 +``` + +## 사용법 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemAwsS3Adapter } from '@deepkit/filesystem-aws-s3'; + +const adapter = new FilesystemAwsS3Adapter({ + bucket: 'my-bucket', + path: 'starting-path/', // 선택 사항 + region: 'eu-central-1', + acccessKeyId: '...', + secretAccessKey: '...' +}); +const filesystem = new Filesystem(adapter); +``` + +참고: 자격 증명을 코드에 직접 저장하지 마세요. 대신 환경 변수 또는 [앱 구성](./app.md#configuration)을 사용하세요. + +이 adapter는 [@aws-sdk/client-s3](https://npmjs.com/package/@aws-sdk/client-s3)의 S3 client를 사용합니다. 모든 configuration 옵션은 adapter 생성자에 전달할 수 있습니다. + +## 권한 + +파일이 생성될 때 어떤 visibility를 가질지 구성할 수 있습니다. + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +파일 `/hello-public.txt`는 ACL `public-read`로 생성되며, URL을 통해 누구나 읽을 수 있습니다. 해당 URL은 `filesystem.publicUrl`을 사용해 가져올 수 있습니다. + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://my-bucket.s3.eu-central-1.amazonaws.com/starting-path/hello-public.txt +``` + +visibility를 사용하려면 S3 bucket에서 object-based ACL을 활성화해야 합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/database.md b/website/src/translations/ko/documentation/filesystem/database.md new file mode 100644 index 000000000..8fde17a73 --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/database.md @@ -0,0 +1,23 @@ +# 데이터베이스 파일시스템 + +이 어댑터를 사용하면 데이터베이스 ORM을 파일시스템 백엔드로 사용할 수 있습니다. 즉, 모든 파일과 폴더는 데이터베이스에 저장됩니다. + +```sh +npm install @deepkit/filesystem-database @deepkit/orm +``` + +## 사용법 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemDatabaseAdapter } from '@deepkit/filesystem-database'; + +const database = new Database(new MemoryDatabaseAdapter()); +// const database = new Database(new PostgresDatabaseAdapter()); +// const database = new Database(new MongoDatabaseAdapter()); +// const database = new Database(new MysqlDatabaseAdapter()); +// const database = new Database(new SQLiteDatabaseAdapter()); + +const adapter = new FilesystemDatabaseAdapter({ database }); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/ftp.md b/website/src/translations/ko/documentation/filesystem/ftp.md new file mode 100644 index 000000000..f9826d4c8 --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/ftp.md @@ -0,0 +1,55 @@ +# FTP 파일 시스템 + +이 어댑터는 FTP 서버를 파일 시스템으로 사용할 수 있게 해줍니다. + +이는 별도로 설치해야 하는 `@deepkit/filesystem-ftp`의 일부입니다. + +```sh +npm install @deepkit/filesystem-ftp +``` + +## 사용법 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemFtpAdapter } from '@deepkit/filesystem-ftp'; + +const adapter = new FilesystemFtpAdapter({ + root: 'folder', + host: 'localhost', + port: 21, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +참고: 인증 정보를 코드에 직접 저장하지 마세요. 대신 환경 변수 또는 [앱 구성](./app.md#configuration)을 사용하세요. + +## 권한 + +FTP 서버가 Unix 환경에서 실행 중이라면, [로컬 파일 시스템 어댑터](./local.md)와 마찬가지로 `permissions` 옵션을 사용하여 파일과 폴더의 권한을 설정할 수 있습니다. + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +여기서는 파일 `/hello-public.txt`가 권한 `0o644`로 생성되고, `/hello-private.txt`는 `0o600`으로 생성됩니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/google-storage.md b/website/src/translations/ko/documentation/filesystem/google-storage.md new file mode 100644 index 000000000..11aaf73ca --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/google-storage.md @@ -0,0 +1,49 @@ +# Google Storage 파일시스템 + +이 어댑터는 Google Storage를 파일시스템으로 사용할 수 있게 해줍니다. + +이는 별도로 설치해야 하는 `@deepkit/filesystem-google`의 일부입니다. + +```sh +npm install @deepkit/filesystem-google +``` + +## 사용법 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemGoogleAdapter } from '@deepkit/filesystem-google'; + +const adapter = new FilesystemGoogleAdapter({ + bucket: 'my-bucket', + path: 'starting-path/', // 선택 사항 + projectId: '...', + keyFilename: 'path/to/service-account-key.json' +}); +const filesystem = new Filesystem(adapter); +``` + +참고: 자격 증명을 코드에 직접 저장하지 마세요. 대신 환경 변수 또는 [앱 구성](./app.md#configuration)을 사용하세요. + +이 어댑터는 [@google-cloud/storage](https://npmjs.com/package/@google-cloud/storage)의 Google Storage 클라이언트를 사용합니다. +해당 클라이언트의 모든 구성 옵션을 어댑터 생성자에 전달할 수 있습니다. + +## 권한 + +파일이 생성될 때 어떤 가시성(visibility)을 가질지 구성할 수 있습니다. + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +파일 `/hello-public.txt`는 ACL `public: true`로 생성되며, URL을 통해 누구나 읽을 수 있습니다. 해당 URL은 `filesystem.publicUrl`을 사용해 가져올 수 있습니다. + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://storage.googleapis.com/my-bucket/starting-path/hello-public.txt +``` + +visibility를 사용하려면 Google Storage 버킷에서 객체 기반(object-based) ACL을 활성화해야 합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/local.md b/website/src/translations/ko/documentation/filesystem/local.md new file mode 100644 index 000000000..6f219f6bf --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/local.md @@ -0,0 +1,49 @@ +# 로컬 파일시스템 + +로컬 파일시스템 어댑터는 가장 일반적인 파일시스템 중 하나로, 애플리케이션이 실행 중인 파일시스템에 접근할 수 있도록 합니다. + +이는 `@deepkit/filesystem`의 일부이며 내부적으로 Node의 `fs/promises` API를 사용하므로 추가 설치가 필요하지 않습니다. + +## 사용법 + +```typescript +import { FilesystemLocalAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter({ root: '/path/to/files' }); +const filesystem = new Filesystem(adapter); +``` + +여기서 `root` 옵션은 파일시스템의 루트 디렉터리입니다. 모든 경로는 이 루트를 기준으로 상대 경로입니다. + +```typescript +// 파일 /path/to/files/hello.txt 를 읽습니다 +const content: string = await filesystem.readAsText('/hello.txt'); +``` + +## 권한 + +파일과 디렉터리를 생성할 때 파일시스템이 사용할 권한을 구성할 수 있습니다. 각 카테고리 (file, directory)는 두 가지 visibility: `public` 및 `private`에 대해 별도로 설정할 수 있습니다. + +```typescript +const adapter = new FilesystemLocalAdapter({ + root: '/path/to/files', + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +여기서 파일 `/hello-public.txt`는 권한 `0o644`로, `/hello-private.txt`는 `0o600`으로 생성됩니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/memory.md b/website/src/translations/ko/documentation/filesystem/memory.md new file mode 100644 index 000000000..fbfe76e6d --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/memory.md @@ -0,0 +1,15 @@ +# 메모리 파일시스템 + +메모리 파일시스템의 경우 파일시스템이 메모리에 저장됩니다. 이는 파일시스템이 영구적이지 않으며 애플리케이션이 종료되면 사라진다는 의미입니다. +이는 특히 테스트 목적에 유용합니다. + +이는 `@deepkit/filesystem`의 일부이므로 추가 설치가 필요하지 않습니다. + +## 사용법 + +```typescript +import { FilesystemMemoryAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemMemoryAdapter(); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/filesystem/sftp.md b/website/src/translations/ko/documentation/filesystem/sftp.md new file mode 100644 index 000000000..732ded7e0 --- /dev/null +++ b/website/src/translations/ko/documentation/filesystem/sftp.md @@ -0,0 +1,57 @@ +# sFTP (SSH) Filesystem + +이 adapter는 sFTP (SSH) 서버를 filesystem으로 사용할 수 있게 해줍니다. + +이는 `@deepkit/filesystem-sftp`의 일부로, 별도로 설치해야 합니다. + +```sh +npm install @deepkit/filesystem-sftp +``` + +## 사용법 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemSftpAdapter } from '@deepkit/filesystem-sftp'; + +const adapter = new FilesystemSftpAdapter({ + root: 'folder', + host: 'localhost', + port: 22, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +참고: 자격 증명을 코드에 직접 저장하지 마세요. 대신 환경 변수 또는 [App Configuration](./app.md#configuration)을 사용하세요. + +이 adapter는 [ssh2-sftp-client](https://npmjs.com/package/ssh2-sftp-client)의 sFTP client를 사용합니다. 해당 패키지의 모든 configuration 옵션을 adapter constructor에 전달할 수 있습니다. + +## Permissions + +FTP 서버가 Unix 환경에서 실행 중이라면, [local filesystem adapter](./local.md)와 마찬가지로 `permissions` 옵션을 사용해 파일과 폴더의 permissions를 설정할 수 있습니다. + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +여기서는 파일 `/hello-public.txt`가 `0o644` permissions로, `/hello-private.txt`가 `0o600`으로 생성됩니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/framework.md b/website/src/translations/ko/documentation/framework.md new file mode 100644 index 000000000..023d19b1f --- /dev/null +++ b/website/src/translations/ko/documentation/framework.md @@ -0,0 +1,215 @@ +# Deepkit 프레임워크 + +Deepkit 프레임워크는 `@deepkit/app`의 [Deepkit App](./app.md)을 기반으로 하며, 애플리케이션에 가져와 사용할 수 있는 `@deepkit/framework`의 `FrameworkModule` 모듈을 제공합니다. + +`App` 추상화는 다음을 제공합니다: + +- CLI 명령 +- 구성 로딩(환경, dotfiles, 사용자 지정) +- 모듈 시스템 +- 강력한 서비스 컨테이너 +- 컨트롤러, 프로바이더, 리스너 등용 레지스트리와 훅 + +`FrameworkModule` 모듈은 다음과 같은 추가 기능을 제공합니다: + +- 애플리케이션 서버 + - HTTP 서버 + - RPC 서버 + - 다중 프로세스 로드 밸런싱 + - SSL +- 디버깅 CLI 명령 +- 데이터베이스 마이그레이션 구성/명령 +- `{debug: true}` 옵션을 통한 디버깅/프로파일러 GUI +- 대화형 API 문서화(Swagger 유사) +- DatabaseRegistry, ProcessLocking, Broker, Sessions용 프로바이더 +- 통합 테스트 API + +`FrameworkModule` 유무와 관계없이 애플리케이션을 작성할 수 있습니다. + +## 설치 + +Deepkit 프레임워크는 [Deepkit App](./app.md)을 기반으로 합니다. 해당 설치 안내를 먼저 따랐는지 확인하세요. +그렇다면 Deepkit 프레임워크를 설치하고 `App`에 `FrameworkModule`을 가져와(import) 사용할 수 있습니다. + +```sh +npm install @deepkit/framework +``` + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +이제 애플리케이션이 `FrameworkModule`을 가져왔으므로, 주제별로 묶인 더 많은 명령이 사용 가능함을 볼 수 있습니다. + +그중 하나는 HTTP 서버를 시작하는 `server:start`입니다. 이를 사용하려면 최소 한 개의 HTTP 라우트를 등록해야 합니다. + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return 'Hello World'; +}) + +app.run(); +``` + +`server:start` 명령을 다시 실행하면 HTTP 서버가 시작되고 `/` 라우트가 사용 가능함을 확인할 수 있습니다. + +```sh +$ ./node_modules/.bin/ts-node ./app.ts server:start +``` + +```sh +$ curl http://localhost:8080/ +Hello World +``` + +요청을 처리하는 방법은 [HTTP](http.md) 또는 [RPC](rpc.md) 장을 참조하세요. [App](app.md) 장에서 CLI 명령에 대해 더 자세히 배울 수 있습니다. + +## App + +`App` 클래스는 애플리케이션의 주요 진입점입니다. 모든 모듈과 구성을 로딩하고 애플리케이션을 시작하는 역할을 합니다. +또한 모든 CLI 명령을 로딩하고 실행하는 역할도 합니다. FrameworkModule과 같은 모듈은 추가 명령을 제공하고, 이벤트 리스너를 등록하며, +HTTP/RPC용 컨트롤러, 서비스 프로바이더 등을 제공합니다. + +이 `app` 객체는 CLI 컨트롤러를 실행하지 않고도 의존성 주입 컨테이너에 접근하는 데 사용할 수 있습니다. + +```typescript +const app = new App({ + imports: [new FrameworkModule] +}); + +//등록된 모든 서비스에 접근하기 +const eventDispatcher = app.get(EventDispatcher); +``` + +`FrameworkModule`이 다른 많은 것들(Logger, ApplicationServer, 그리고 [더 많은 것들](https://github.com/deepkit/deepkit-framework/blob/master/packages/framework/src/module.ts))과 마찬가지로 이를 서비스 프로바이더로 등록하므로 `EventDispatcher`를 가져올 수 있습니다. + +고유한 서비스를 등록할 수도 있습니다. + +```typescript + +class MyService { + constructor(private logger: Logger) { + } + + helloWorld() { + this.logger.log('Hello World'); + } +} + +const app = new App({ + providers: [MyService], + imports: [new FrameworkModule] +}); + +const service = app.get(MyService); + +service.helloWorld(); +``` + +### 디버거 + +애플리케이션 및 모든 모듈의 구성 값을 디버거에서 확인할 수 있습니다. `FrameworkModule`에서 debug 옵션을 활성화하고 `http://localhost:8080/_debug/configuration`을 열어보세요. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + debug: true, + }) + ] +}).run(); +``` + +![디버거 구성](/assets/documentation/framework/debugger-configuration.png) + +`ts-node app.ts app:config`를 사용하여 사용 가능한 모든 구성 옵션, 활성 값, 기본값, 설명 및 데이터 타입을 표시할 수도 있습니다. + +```sh +$ ts-node app.ts app:config +Application config +┌─────────┬───────────────┬────────────────────────┬────────────────────────┬─────────────┬───────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼───────────────┼────────────────────────┼────────────────────────┼─────────────┼───────────┤ +│ 0 │ 'pageTitle' │ 'Other title' │ 'Cool site' │ '' │ 'string' │ +│ 1 │ 'domain' │ 'example.com' │ 'example.com' │ '' │ 'string' │ +│ 2 │ 'port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 3 │ 'databaseUrl' │ 'mongodb://localhost/' │ 'mongodb://localhost/' │ '' │ 'string' │ +│ 4 │ 'email' │ false │ false │ '' │ 'boolean' │ +│ 5 │ 'emailSender' │ undefined │ undefined │ '' │ 'string?' │ +└─────────┴───────────────┴────────────────────────┴────────────────────────┴─────────────┴───────────┘ +Modules config +┌─────────┬──────────────────────────────┬─────────────────┬─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼──────────────────────────────┼─────────────────┼─────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────┤ +│ 0 │ 'framework.host' │ 'localhost' │ 'localhost' │ '' │ 'string' │ +│ 1 │ 'framework.port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 2 │ 'framework.httpsPort' │ undefined │ undefined │ 'If httpsPort and ssl is defined, then the https server is started additional to the http-server.' │ 'number?' │ +│ 3 │ 'framework.selfSigned' │ undefined │ undefined │ 'If for ssl: true the certificate and key should be automatically generated.' │ 'boolean?' │ +│ 4 │ 'framework.keepAliveTimeout' │ undefined │ undefined │ '' │ 'number?' │ +│ 5 │ 'framework.path' │ '/' │ '/' │ '' │ 'string' │ +│ 6 │ 'framework.workers' │ 1 │ 1 │ '' │ 'number' │ +│ 7 │ 'framework.ssl' │ false │ false │ 'Enables HTTPS server' │ 'boolean' │ +│ 8 │ 'framework.sslOptions' │ undefined │ undefined │ 'Same interface as tls.SecureContextOptions & tls.TlsOptions.' │ 'any' │ +... +``` + +## 애플리케이션 서버 + +## 파일 구조 + +## 자동 CRUD + +## 이벤트 + +Deepkit 프레임워크에는 이벤트 리스너를 등록할 수 있는 다양한 이벤트 토큰이 포함되어 있습니다. + +이벤트 동작 방식에 대한 자세한 내용은 [이벤트](./app/events.md) 장을 참고하세요. + +### 이벤트 디스패치 + +이벤트는 `EventDispatcher` 클래스 통해 전송됩니다. Deepkit 앱에서는 의존성 주입을 통해 이를 제공받을 수 있습니다. + +```typescript +import { cli, Command } from '@deepkit/app'; +import { EventDispatcher } from '@deepkit/event'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected eventDispatcher: EventDispatcher) { + } + + async execute() { + this.eventDispatcher.dispatch(UserAdded, new UserEvent({ username: 'Peter' })); + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/framework/database.md b/website/src/translations/ko/documentation/framework/database.md new file mode 100644 index 000000000..acf3d1e5b --- /dev/null +++ b/website/src/translations/ko/documentation/framework/database.md @@ -0,0 +1,225 @@ +# 데이터베이스 + +Deepkit에는 Deepkit ORM이라고 불리는 강력한 데이터베이스 추상화 라이브러리가 있습니다. 이는 SQL 데이터베이스와 MongoDB 작업을 용이하게 하는 객체-관계 매핑(ORM) 라이브러리입니다. + +원하는 어떤 데이터베이스 라이브러리도 사용할 수 있지만, Deepkit ORM은 Deepkit 프레임워크와 완벽하게 통합되어 있으며 워크플로와 효율성을 높여 줄 많은 기능을 갖춘 가장 빠른 TypeScript 데이터베이스 추상화 라이브러리이므로 사용을 권장합니다. + +이 장에서는 Deepkit 앱에서 Deepkit ORM을 사용하는 방법을 설명합니다. Deepkit ORM에 대한 모든 정보는 [ORM](../orm.md) 장을 참고하세요. + +## 데이터베이스 클래스 + +애플리케이션 내에서 Deepkit ORM의 `Database` 객체를 사용하는 가장 간단한 방법은 이를 상속한 클래스를 등록하는 것입니다. + +```typescript +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + constructor() { + super( + new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), + [User] + ); + } +} +``` + +새 클래스를 만들고, 생성자에서 어댑터와 해당 매개변수를 지정한 다음 두 번째 매개변수에 이 데이터베이스에 연결해야 하는 모든 엔티티 모델을 추가하세요. + +이제 이 데이터베이스 클래스를 프로바이더로 등록할 수 있습니다. 또한 `migrateOnStartup`을 활성화하여 부트스트랩 시 데이터베이스의 모든 테이블을 자동으로 생성하도록 합니다. 이는 빠른 프로토타이핑에 이상적이지만, 본격적인 프로젝트나 프로덕션 환경에서는 권장되지 않습니다. 일반적인 데이터베이스 마이그레이션을 사용해야 합니다. + +또한 `debug`를 활성화하여 애플리케이션 서버가 시작될 때 디버거를 열고, 내장된 ORM 브라우저에서 데이터베이스 모델을 직접 관리할 수 있도록 합니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { SQLiteDatabase } from './database.ts'; + +new App({ + providers: [SQLiteDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}).run(); +``` + +이제 의존성 주입(Dependency Injection)을 사용하여 어디서든 `SQLiteDatabase`에 접근할 수 있습니다: + +```typescript +import { SQLiteDatabase } from './database.ts'; + +export class Controller { + constructor(protected database: SQLiteDatabase) {} + + @http.GET() + async startPage(): Promise<User[]> { + //모든 사용자를 반환 + return await this.database.query(User).find(); + } +} +``` + +## 구성 + +많은 경우 연결 자격 증명을 구성 가능하게 만들고 싶을 것입니다. 예를 들어, 프로덕션과 테스트에서 다른 데이터베이스를 사용하고 싶을 수 있습니다. 이는 `Database` 클래스의 `config` 옵션을 사용하여 수행할 수 있습니다. + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { PostgresDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +type DbConfig = Pick<AppConfig, 'databaseHost', 'databaseUser', 'databasePassword'>; + +export class MainDatabase extends Database { + constructor(config: DbConfig) { + super(new PostgresDatabaseAdapter({ + host: config.databaseHost, + user: config.databaseUser, + password: config.databasePassword, + }), [User]); + } +} +``` + +```typescript +//config.ts +export class AppConfig { + databaseHost: string = 'localhost'; + databaseUser: string = 'postgres'; + databasePassword: string = ''; +} +``` + +```typescript +//app.ts +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { MainDatabase } from './database.ts'; +import { AppConfig } from './config.ts'; + +const app = new App({ + config: AppConfig, + providers: [MainDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}); +app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper', envFilePath: ['local.env', 'prod.env']}); +app.run(); +``` + +이제 loadConfigFromEnv를 사용하므로 환경 변수를 통해 데이터베이스 자격 증명을 설정할 수 있습니다. + +```sh +APP_DATABASE_HOST=localhost APP_DATABASE_USER=postgres ts-node app.ts server:start +``` + +또는 `local.env` 파일에 설정하고, 사전에 환경 변수를 설정하지 않고 `ts-node app.ts server:start`를 실행하세요. + +```sh +APP_DATABASE_HOST=localhost +APP_DATABASE_USER=postgres +``` + +## 다중 데이터베이스 + +원하는 만큼 많은 데이터베이스 클래스를 추가하고 원하는 대로 이름을 지정할 수 있습니다. Deepkit ORM 브라우저를 사용할 때 다른 것들과 충돌하지 않도록 각 데이터베이스의 이름을 변경해야 합니다. + +## 데이터 관리 + +이제 Deepkit ORM 브라우저로 데이터베이스 데이터를 관리할 모든 준비가 완료되었습니다. Deepkit ORM 브라우저를 열고 내용을 관리하려면 위의 모든 단계를 `app.ts` 파일에 작성하고 서버를 시작하세요. + +```sh +$ ts-node app.ts server:start +2021-06-11T15:08:54.330Z [LOG] Start HTTP server, using 1 workers. +2021-06-11T15:08:54.333Z [LOG] Migrate database default +2021-06-11T15:08:54.336Z [LOG] RPC DebugController deepkit/debug/controller +2021-06-11T15:08:54.337Z [LOG] RPC OrmBrowserController orm-browser/controller +2021-06-11T15:08:54.337Z [LOG] HTTP OrmBrowserController +2021-06-11T15:08:54.337Z [LOG] GET /_orm-browser/query httpQuery +2021-06-11T15:08:54.337Z [LOG] HTTP StaticController +2021-06-11T15:08:54.337Z [LOG] GET /_debug/:any serviceApp +2021-06-11T15:08:54.337Z [LOG] HTTP listening at http://localhost:8080/ +``` + +이제 http://localhost:8080/_debug/database/default 를 열 수 있습니다. + +![디버거 데이터베이스](/assets/documentation/framework/debugger-database.png) + +ER(엔터티 관계) 다이어그램을 볼 수 있습니다. 현재는 엔티티가 하나만 있습니다. 관계를 가진 엔티티를 더 추가하면 모든 정보를 한눈에 볼 수 있습니다. + +왼쪽 사이드바에서 `User`를 클릭하면 해당 내용을 관리할 수 있습니다. `+` 아이콘을 클릭하고 새 레코드의 제목을 변경하세요. 필요한 값(예: 사용자 이름)을 변경한 후 `Confirm`을 클릭합니다. 그러면 모든 변경 사항이 데이터베이스에 커밋되어 영구적으로 반영됩니다. 자동 증가 ID는 자동으로 할당됩니다. + +![디버거 데이터베이스 사용자](/assets/documentation/framework/debugger-database-user.png) + +## 더 알아보기 + +`SQLiteDatabase`가 어떻게 동작하는지 더 알고 싶다면 [Database](../orm.md) 장과 데이터 쿼리, 세션을 통한 데이터 조작, 관계 정의 등과 같은 하위 장을 읽어보세요. +해당 장들은 독립 실행형 라이브러리 `@deepkit/orm`을 다루며, 위에서 읽은 Deepkit 프레임워크 부분에 대한 문서는 포함하지 않는다는 점에 유의하세요. 독립 실행형 라이브러리에서는 예를 들어 `new SQLiteDatabase()`와 같이 데이터베이스 클래스를 직접 인스턴스화합니다. 그러나 Deepkit 앱에서는 의존성 주입 컨테이너를 통해 이것이 자동으로 처리됩니다. + +## 마이그레이션 + +Deepkit 프레임워크에는 마이그레이션을 생성, 실행 및 되돌릴 수 있는 강력한 마이그레이션 시스템이 있습니다. 마이그레이션 시스템은 Deepkit ORM 라이브러리를 기반으로 하며, 따라서 프레임워크와 완벽하게 통합되어 있습니다. + +`FrameworkModule`은 마이그레이션을 관리하기 위한 여러 명령을 제공합니다. + +- `migration:create` - 데이터베이스 차이(diff)를 기반으로 새 마이그레이션 파일을 생성 +- `migration:pending` - 보류 중인 마이그레이션 파일 표시 +- `migration:up` - 보류 중인 마이그레이션 파일을 실행 +- `migration:down` - 다운 마이그레이션을 실행하여 이전 마이그레이션 파일을 되돌림 + +```sh +ts-node app.ts migration:create --migrationDir src/migrations +``` + +새 마이그레이션 파일이 `migrations`에 생성됩니다. 이 폴더는 FrameworkModule에 구성된 기본 디렉터리입니다. 이를 변경하려면 (구성 장에서 설명한 대로) 환경 변수를 통해 구성하거나 `FrameworkModule` 생성자에 `migrationDir` 옵션을 전달하세요. + +```typescript +new FrameworkModule({ + migrationDir: 'src/migrations', +}) +``` + +새로 생성된 마이그레이션 파일에는 TypeScript 앱에서 정의된 엔티티와 구성된 데이터베이스 간의 차이에 기반한 up과 down 메서드가 포함됩니다. +이제 필요에 맞게 up 메서드를 수정할 수 있습니다. down 메서드는 up 메서드를 기반으로 자동 생성됩니다. +이 파일을 저장소에 커밋하여 다른 개발자도 실행할 수 있도록 합니다. + +### 보류 중인 마이그레이션 + +```sh +ts-node app.ts migration:pending --migrationDir src/migrations +``` + +모든 보류 중인 마이그레이션을 표시합니다. 아직 실행되지 않은 새 마이그레이션 파일이 있으면 여기에 나열됩니다. + +### 마이그레이션 실행 + +```sh +ts-node app.ts migration:up --migrationDir src/migrations +``` + +다음 보류 중인 마이그레이션을 실행합니다. + +### 마이그레이션 되돌리기 + +```sh +ts-node app.ts migration:down --migrationDir src/migrations +``` + +마지막으로 실행된 마이그레이션을 되돌립니다. + +### 페이크 마이그레이션 + +예를 들어 마이그레이션(up 또는 down)을 실행하려고 했지만 실패했다고 가정해 봅시다. 문제를 수동으로 수정했지만, 이제 이미 실행된 것으로 표시되어 다시 마이그레이션을 실행할 수 없습니다. `--fake` 옵션을 사용하면 실제로 실행하지 않고도 데이터베이스에서 해당 마이그레이션을 실행된 것으로 표시할 수 있습니다. 이렇게 하면 다음 보류 중인 마이그레이션을 실행할 수 있습니다. + +```sh +ts-node app.ts migration:up --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/framework/deployment.md b/website/src/translations/ko/documentation/framework/deployment.md new file mode 100644 index 000000000..40f0ea9c9 --- /dev/null +++ b/website/src/translations/ko/documentation/framework/deployment.md @@ -0,0 +1,138 @@ +# 배포 + +이 장에서는 애플리케이션을 JavaScript로 컴파일하고, 프로덕션 환경에 맞게 구성한 뒤, Docker를 사용해 배포하는 방법을 배웁니다. + +## TypeScript 컴파일 + +`app.ts` 파일에 다음과 같은 애플리케이션이 있다고 가정해봅시다: + +```typescript +#!/usr/bin/env ts-node-script +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class Config { + title: string = 'DEV my Page'; +} + +class MyWebsite { + constructor(protected title: Config['title']) { + } + + @http.GET() + helloWorld() { + return 'Hello from ' + this.title; + } +} + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + .loadConfigFromEnv() + .run(); +``` + +`ts-node app.ts server:start` 를 사용하면 모든 것이 정상 작동하는 것을 확인할 수 있습니다. 프로덕션 환경에서는 일반적으로 `ts-node`로 서버를 시작하지 않습니다. 대신 JavaScript로 컴파일한 다음 Node로 실행합니다. 이를 위해서는 올바른 설정 옵션이 포함된 `tsconfig.json`이 필요합니다. “첫 번째 애플리케이션” 섹션에서는 `tsconfig.json`이 JavaScript 출력을 `.dist` 폴더로 내보내도록 설정되어 있습니다. 여기서도 동일하게 설정되어 있다고 가정합니다. + +컴파일러 설정이 모두 올바르고 `outDir`가 `dist`와 같은 폴더를 가리킨다면, 프로젝트에서 `tsc` 명령을 실행하는 즉시 `tsconfig.json`에 연결된 파일들이 모두 JavaScript로 컴파일됩니다. 이 목록에는 엔트리 파일만 지정하면 충분합니다. import된 파일은 자동으로 함께 컴파일되므로 `tsconfig.json`에 명시적으로 추가할 필요가 없습니다. `tsc`는 `npm install typescript`로 설치할 때 함께 제공되는 TypeScript의 일부입니다. + +```sh +$ ./node_modules/.bin/tsc +``` + +TypeScript 컴파일러는 성공했을 때 아무 출력도 하지 않습니다. 이제 `dist`의 출력을 확인할 수 있습니다. + +```sh +$ tree dist +dist +└── app.js +``` + +파일이 하나만 있는 것을 볼 수 있습니다. `node distapp.js`로 실행하면 `ts-node app.ts`와 동일한 기능을 얻을 수 있습니다. + +배포를 위해서는 TypeScript 파일이 올바르게 컴파일되고 모든 것이 Node에서 직접 동작하는 것이 중요합니다. 이제 `node_modules`를 포함한 `dist` 폴더를 그대로 옮겨 `node distapp.js server:start`를 실행하면 앱이 성공적으로 배포됩니다. 하지만 일반적으로는 Docker와 같은 다른 솔루션을 사용해 앱을 올바르게 패키징합니다. + +## 구성 + +프로덕션 환경에서는 서버를 `localhost`에 바인딩하지 않고 보통 `0.0.0.0`을 통해 모든 인터페이스에 바인딩합니다. 리버스 프록시 뒤에 있지 않다면 포트도 80으로 설정할 것입니다. 이 두 설정을 구성하려면 `FrameworkModule`을 커스터마이즈해야 합니다. 관심 있는 두 옵션은 `host`와 `port`입니다. 이 값을 환경 변수나 .dotenv 파일을 통해 외부에서 설정할 수 있도록 하려면, 먼저 이를 허용해야 합니다. 다행히 위 코드에서는 `loadConfigFromEnv()` 메서드로 이미 설정해 두었습니다. + +애플리케이션 구성 옵션을 설정하는 방법에 대해 더 알아보려면 [구성](../app/configuration.md) 장을 참고하세요. + +사용 가능한 구성 옵션과 그 값들을 확인하려면 `ts-node app.ts app:config` 명령을 사용할 수 있습니다. Framework Debugger에서도 확인할 수 있습니다. + +### SSL + +애플리케이션을 SSL이 적용된 HTTPS로 실행하는 것이 권장되며(때로는 필수이기도 합니다). SSL을 구성하기 위한 여러 옵션이 있습니다. SSL을 활성화하려면 +`framework.ssl`을 사용하고 다음 옵션들로 매개변수를 구성하세요. + +|=== +|이름|Type|설명 + +|framework.ssl|boolean|true일 때 HTTPS 서버를 활성화합니다 +|framework.httpsPort|number?|httpsPort와 ssl이 정의되어 있으면, http 서버에 추가로 https 서버가 시작됩니다. +|framework.sslKey|string?|HTTPS용 SSL key 파일의 경로 +|framework.sslCertificate|string?|HTTPS용 certificate 파일의 경로 +|framework.sslCa|string?|HTTPS용 CA 파일의 경로 +|framework.sslCrl|string?|HTTPS용 CRL 파일의 경로 +|framework.sslOptions|object?|tls.SecureContextOptions 및 tls.TlsOptions와 동일한 인터페이스. +|=== + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// 여기에서 config와 HTTP controller를 정의합니다 + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + sslKey: __dirname + 'path/ssl.key', + sslCertificate: __dirname + 'path/ssl.cert', + sslCA: __dirname + 'path/ssl.ca', + }) + ] +}) + .run(); +``` + +### 로컬 SSL + +로컬 개발 환경에서는 `framework.selfSigned` 옵션으로 self-signed HTTPS를 활성화할 수 있습니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// 여기에서 config와 HTTP controller를 정의합니다 + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + }) + ] +}) + .run(); +``` + +```sh +$ ts-node app.ts server:start +2021-06-13T18:04:01.563Z [LOG] Start HTTP server, using 1 workers. +2021-06-13T18:04:01.598Z [LOG] Self signed certificate for localhost created at var/self-signed-localhost.cert +2021-06-13T18:04:01.598Z [LOG] Tip: If you want to open this server via chrome for localhost, use chrome://flags/#allow-insecure-localhost +2021-06-13T18:04:01.606Z [LOG] HTTP MyWebsite +2021-06-13T18:04:01.606Z [LOG] GET / helloWorld +2021-06-13T18:04:01.606Z [LOG] HTTPS listening at https://localhost:8080/ +``` + +이 서버를 지금 시작하면 HTTP 서버가 `https:localhost:8080`에서 HTTPS로 제공됩니다. Chrome에서 이 URL을 열면 self-signed 인증서는 보안 위험으로 간주되기 때문에 "NET::ERR_CERT_INVALID" 오류 메시지가 표시됩니다: `chrome:flagsallow-insecure-localhost`. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/framework/public.md b/website/src/translations/ko/documentation/framework/public.md new file mode 100644 index 000000000..a55893237 --- /dev/null +++ b/website/src/translations/ko/documentation/framework/public.md @@ -0,0 +1,34 @@ +# Public 디렉터리 + +`FrameworkModule`는 HTTP를 통해 이미지, PDF, 바이너리 등과 같은 정적 파일(static files)을 서빙할 수 있는 방법을 제공합니다. `publicDir` 설정 옵션은 HTTP controller route로 연결되지 않는 요청에 대해 기본 entry point로 사용할 폴더를 지정할 수 있게 해줍니다. 기본적으로 이 동작은 비활성화되어 있습니다(빈 값). + +공개 파일 제공을 활성화하려면, `publicDir`을 원하는 폴더로 설정하세요. 보통은 명확하도록 폴더 이름을 `publicDir`처럼 정합니다. + +``` +. +├── app.ts +└── publicDir + └── logo.jpg +``` + +`publicDir` 옵션을 변경하려면, `FrameworkModule`의 첫 번째 인자를 변경하면 됩니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// 여기에 config와 HTTP controller를 정의하세요 + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + publicDir: 'publicDir' + }) + ] +}) + .run(); +``` + +이제 이 설정된 폴더 내의 모든 파일은 HTTP로 접근할 수 있습니다. 예를 들어, `http:localhost:8080/logo.jpg`를 열면 `publicDir` 디렉터리의 `logo.jpg` 이미지를 볼 수 있습니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/framework/testing.md b/website/src/translations/ko/documentation/framework/testing.md new file mode 100644 index 000000000..4b2b802a2 --- /dev/null +++ b/website/src/translations/ko/documentation/framework/testing.md @@ -0,0 +1,179 @@ +# 테스트 + +Deepkit framework의 services와 controllers는 SOLID와 clean code 원칙을 따르며, 잘 설계되고 캡슐화되어 분리된 구조를 지원하도록 설계되었습니다. 이러한 특징은 코드를 테스트하기 쉽게 만듭니다. + +이 문서는 `ts-jest`와 함께 [Jest](https://jestjs.io)라는 테스트 프레임워크를 설정하는 방법을 보여줍니다. 이를 위해 다음 명령어를 실행해 `jest`와 `ts-jest`를 설치하세요. + +```sh +npm install jest ts-jest @types/jest +``` + +Jest는 테스트 스위트를 어디에서 찾고 TS 코드를 어떻게 컴파일할지 알기 위해 몇 가지 설정이 필요합니다. `package.json`에 다음 설정을 추가하세요: + +```json title=package.json +{ + ..., + + "jest": { + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "testEnvironment": "node", + "testMatch": [ + "**/*.spec.ts" + ] + } +} +``` + +테스트 파일 이름은 `.spec.ts`여야 합니다. 다음 내용으로 `test.spec.ts` 파일을 만드세요. + +```typescript +test('first test', () => { + expect(1 + 1).toBe(2); +}); +``` + +이제 jest 명령어로 모든 테스트 스위트를 한 번에 실행할 수 있습니다. + +```sh +$ node_modules/.bin/jest + PASS ./test.spec.ts + ✓ first test (1 ms) + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: 0.23 s, estimated 1 s +Ran all test suites. +``` + +Jest CLI 도구가 어떻게 동작하는지와 더 정교한 테스트 및 전체 테스트 스위트를 작성하는 방법을 알아보려면 [Jest 문서](https://jestjs.io)를 읽어보세요. + +## 단위 테스트 + +가능하다면 services를 단위 테스트해야 합니다. 서비스 의존성이 단순하고, 더 잘 분리되어 있으며, 더 명확하게 정의될수록 테스트가 쉬워집니다. 이런 경우 다음과 같은 간단한 테스트를 작성할 수 있습니다: + +```typescript +export class MyService { + helloWorld() { + return 'hello world'; + } +} +``` + +```typescript +// +import { MyService } from './my-service.ts'; + +test('hello world', () => { + const myService = new MyService(); + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +## 통합 테스트 + +항상 단위 테스트를 작성할 수 있는 것도 아니고, 항상 비즈니스 크리티컬한 코드와 동작을 커버하는 가장 효율적인 방법인 것도 아닙니다. 특히 아키텍처가 매우 복잡한 경우에는 end-to-end 통합 테스트를 쉽게 수행할 수 있는 것이 유리합니다. + +Dependency Injection 장에서 이미 배웠듯이, Dependency Injection Container는 Deepkit의 핵심입니다. 이곳에서 모든 services가 빌드되고 실행됩니다. 애플리케이션은 services(providers), controllers, listeners, 그리고 imports를 정의합니다. 통합 테스트에서는 테스트 케이스에서 모든 services를 사용할 필요는 없고, 보통은 핵심 영역을 테스트할 수 있도록 애플리케이션의 축소판 버전을 사용하고자 합니다. + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { http, HttpRequest } from '@deepkit/http'; + +test('http controller', async () => { + class MyController { + + @http.GET() + hello(@http.query() text: string) { + return 'hello ' + text; + } + } + + const testing = createTestingApp({ controllers: [MyController] }); + await testing.startServer(); + + const response = await testing.request(HttpRequest.GET('/').query({text: 'world'})); + + expect(response.getHeader('content-type')).toBe('text/plain; charset=utf-8'); + expect(response.body.toString()).toBe('hello world'); +}); +``` + +```typescript +import { createTestingApp } from '@deepkit/framework'; + +test('service', async () => { + class MyService { + helloWorld() { + return 'hello world'; + } + } + + const testing = createTestingApp({ providers: [MyService] }); + + // Dependency Injection 컨테이너에 접근하여 MyService를 인스턴스화합니다 + const myService = testing.app.get(MyService); + + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +애플리케이션을 여러 모듈로 나눴다면 더 쉽게 테스트할 수 있습니다. 예를 들어 `AppCoreModule`을 만들었고 몇몇 services를 테스트하고 싶다고 가정해봅시다. + +```typescript +class Config { + items: number = 10; +} + +export class MyService { + constructor(protected items: Config['items']) { + + } + + doIt(): boolean { + // 무언가 수행 + return true; + } +} + +export AppCoreModule = new AppModule({}, { + config: config, + provides: [MyService] +}, 'core'); +``` + +모듈은 다음과 같이 사용합니다: + +```typescript +import { AppCoreModule } from './app-core.ts'; + +new App({ + imports: [new AppCoreModule] +}).run(); +``` + +그리고 전체 애플리케이션 서버를 부팅하지 않고 테스트합니다. + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { AppCoreModule, MyService } from './app-core.ts'; + +test('service simple', async () => { + const testing = createTestingApp({ imports: [new AppCoreModule] }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); + +test('service simple big', async () => { + // 특정 테스트 시나리오를 위해 모듈의 설정을 변경합니다 + const testing = createTestingApp({ + imports: [new AppCoreModule({items: 100})] + }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http.md b/website/src/translations/ko/documentation/http.md new file mode 100644 index 000000000..113ba2acd --- /dev/null +++ b/website/src/translations/ko/documentation/http.md @@ -0,0 +1,69 @@ +# HTTP + +HTTP 요청을 처리하는 일은 서버가 수행하는 가장 잘 알려진 작업 중 하나입니다. 이는 입력(HTTP request)을 출력(HTTP response)으로 변환하여 특정 작업을 수행합니다. 클라이언트는 다양한 방식으로 HTTP request를 통해 서버로 데이터를 보낼 수 있으며, 이는 올바르게 읽고 처리되어야 합니다. HTTP body뿐 아니라 HTTP query나 HTTP header 값으로도 전달될 수 있습니다. 데이터가 실제로 어떻게 처리되는지는 서버에 따라 달라집니다. 값이 어디로, 어떤 방식으로 클라이언트에 의해 전송되어야 하는지 정의하는 것은 서버입니다. + +여기서 가장 중요한 우선순위는 사용자가 기대하는 동작을 정확히 수행하는 것뿐만 아니라, HTTP request로부터의 모든 입력을 올바르게 변환(deserialize)하고 검증(validate)하는 것입니다. + +서버에서 HTTP request가 통과하는 pipeline은 다양하고 복잡할 수 있습니다. 많은 단순한 HTTP 라이브러리는 특정 route에 대해 HTTP request와 HTTP response만을 전달하고, 개발자가 HTTP response를 직접 처리할 것을 기대합니다. middleware API는 필요에 따라 이 pipeline을 확장할 수 있게 해줍니다. + +_Express 예제_ + +```typescript +const http = express(); +http.get('/user/:id', (request, response) => { + response.send({id: request.params.id, username: 'Peter' ); +}); +``` + +이는 단순한 사용 사례에는 매우 잘 맞지만, 애플리케이션이 커질수록 모든 입력과 출력을 수동으로 serialize/deserialize하고 validate해야 하기 때문에 금방 혼란스러워집니다. 또한 database abstraction과 같은 객체와 서비스를 애플리케이션 자체에서 어떻게 획득할지에 대한 고려도 필요합니다. 이는 이러한 필수 기능들을 매핑하는 아키텍처를 그 위에 얹도록 개발자를 강제합니다. + +반면 Deepkit의 HTTP Library는 TypeScript와 Dependency Injection의 강점을 활용합니다. 정의된 types를 기반으로 어떤 값에 대해서도 Serialization/Deserialization과 validation이 자동으로 수행됩니다. 또한 위의 예시처럼 functional API로 route를 정의하거나, 아키텍처의 다양한 요구를 충족하기 위해 controller classes를 통해 정의할 수도 있습니다. + +이는 Node의 `http` module 같은 기존 HTTP 서버와 함께 또는 Deepkit framework와 함께 사용할 수 있습니다. 두 API 변형 모두 Dependency Injection container에 접근할 수 있으므로, 애플리케이션에서 database abstraction과 구성(configuration) 같은 객체를 편리하게 가져올 수 있습니다. + +## Functional API 예시 + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; + +//Functional API +const app = new App({ + imports: [new FrameworkModule()] +}); +const router = app.get(HttpRouterRegistry); + +router.get('/user/:id', (id: number & Positive, database: Database) => { + // id는 number이며 양수임이 보장됩니다. + // database는 DI Container에 의해 주입됩니다. + return database.query(User).filter({ id }).findOne(); +}); + +app.run(); +``` + +## Class Controller API + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; +import { User } from "discord.js"; + +//Controller API +class UserController { + constructor(private database: Database) { + } + + @http.GET('/user/:id') + user(id: number & Positive) { + return this.database.query(User).filter({ id }).findOne(); + } +} + +const app = new App({ + controllers: [UserController], + imports: [new FrameworkModule()] +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http/dependency-injection.md b/website/src/translations/ko/documentation/http/dependency-injection.md new file mode 100644 index 000000000..2489f958d --- /dev/null +++ b/website/src/translations/ko/documentation/http/dependency-injection.md @@ -0,0 +1,67 @@ +# 의존성 주입 + +router Function뿐만 아니라 controller Class와 controller Method도 임의의 의존성을 정의할 수 있으며, 이는 의존성 주입 컨테이너에 의해 해결됩니다. 예를 들어 database 추상화나 logger에 편리하게 접근할 수 있습니다. + +예를 들어, database가 provider로 제공되어 있다면 주입할 수 있습니다: + +```typescript +class Database { + // ... +} + +const app = new App({ + providers: [ + Database, + ], +}); +``` + +_함수형 API:_ + +```typescript +router.get('/user/:id', async (id: number, database: Database) => { + return await database.query(User).filter({id}).findOne(); +}); +``` + +_컨트롤러 API:_ + +```typescript +class UserController { + constructor(private database: Database) {} + + @http.GET('/user/:id') + async userDetail(id: number) { + return await this.database.query(User).filter({id}).findOne(); + } +} + +// 또는 Method에서 직접 +class UserController { + @http.GET('/user/:id') + async userDetail(id: number, database: Database) { + return await database.query(User).filter({id}).findOne(); + } +} +``` + +자세한 내용은 [의존성 주입](dependency-injection)을 참조하세요. + +## 스코프 + +모든 HTTP 컨트롤러와 함수형 Route는 `http` 의존성 주입 스코프 내에서 관리됩니다. HTTP 컨트롤러는 각 HTTP 요청마다 인스턴스화됩니다. 이는 둘 다 `http` 스코프에 등록된 provider에 접근할 수 있음을 의미합니다. 따라서 `@deepkit/http`의 `HttpRequest`와 `HttpResponse`도 의존성으로 사용할 수 있습니다. deepkit framework를 사용하는 경우 `@deepkit/framework`의 `SessionHandler`도 사용할 수 있습니다. + +```typescript +import { HttpResponse } from '@deepkit/http'; + +router.get('/user/:id', (id: number, request: HttpRequest) => { +}); + +router.get('/', (response: HttpResponse) => { + response.end('Hello'); +}); +``` + +각 HTTP 요청마다 서비스를 인스턴스화하기 위해 provider를 `http` 스코프에 배치하는 것이 유용할 수 있습니다. HTTP 요청이 처리되면 `http` 스코프의 DI 컨테이너는 삭제되며, 그에 따라 모든 provider 인스턴스가 가비지 컬렉터(GC)에서 정리됩니다. + +`http` 스코프에 provider를 배치하는 방법은 [의존성 주입 스코프](dependency-injection.md#di-scopes)를 참조하세요. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http/events.md b/website/src/translations/ko/documentation/http/events.md new file mode 100644 index 000000000..9bbb41bc2 --- /dev/null +++ b/website/src/translations/ko/documentation/http/events.md @@ -0,0 +1,84 @@ +# 이벤트 + +HTTP 모듈은 워크플로 엔진을 기반으로 하며, HTTP 요청을 처리하는 전체 과정에 훅(hook)할 수 있도록 다양한 이벤트 토큰을 제공합니다. + +워크플로 엔진은 유한 상태 머신으로, 각 HTTP 요청마다 새로운 상태 머신 인스턴스를 생성한 뒤 위치(position) 간을 이동합니다. 첫 위치는 `start`이고 마지막은 `response`입니다. 각 위치에서 추가 코드를 실행할 수 있습니다. + +![HTTP 워크플로](/assets/documentation/framework/http-workflow.png) + +각 이벤트 토큰은 추가 정보를 담은 고유한 이벤트 타입을 가집니다. + +| 이벤트 토큰 | 설명 | +|-------------------------------|----------------------------------------------------------------------------------------------------------------| +| httpWorkflow.onRequest | 새 요청이 들어왔을 때 | +| httpWorkflow.onRoute | 요청에서 라우트를 해석(resolve)해야 할 때 | +| httpWorkflow.onRouteNotFound | 라우트를 찾지 못했을 때 | +| httpWorkflow.onAuth | 인증이 수행될 때 | +| httpWorkflow.onResolveParameters | 라우트 파라미터가 해석될 때 | +| httpWorkflow.onAccessDenied | 접근이 거부될 때 | +| httpWorkflow.onController | 컨트롤러 액션이 호출될 때 | +| httpWorkflow.onControllerError | 컨트롤러 액션이 에러를 던졌을 때 | +| httpWorkflow.onParametersFailed | 라우트 파라미터 해석이 실패했을 때 | +| httpWorkflow.onResponse | 컨트롤러 액션이 호출된 후. 이 시점에서 결과가 응답으로 변환됩니다. | + +모든 HTTP 이벤트가 워크플로 엔진을 기반으로 하므로, 지정된 이벤트를 사용해 `event.next()` 메서드로 해당 위치로 점프하여 동작을 변경할 수 있습니다. + +HTTP 모듈은 이 이벤트 토큰들에 자체 이벤트 리스너를 등록해 HTTP 요청 처리를 구현합니다. 이러한 모든 이벤트 리스너의 우선순위(priority)는 100이며, 이는 여러분이 이벤트를 수신(listen)할 때 기본적으로 여러분의 리스너가 먼저 실행된다는 의미입니다(기본 우선순위는 0). HTTP 기본 핸들러 이후에 실행하려면 100보다 큰 우선순위를 지정하세요. + +예를 들어, 컨트롤러가 호출될 때의 이벤트를 가로채고 싶다고 가정해 봅시다. 특정 컨트롤러가 호출될 경우, 사용자에게 해당 컨트롤러에 대한 접근 권한이 있는지 확인합니다. 권한이 있다면 계속 진행합니다. 하지만 권한이 없다면 다음 워크플로 항목 `accessDenied`로 점프합니다. 그러면 접근 거부 절차가 그 이후 단계에서 자동으로 처리됩니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HtmlResponse, http, httpAction, httpWorkflow } from '@deepkit/http'; +import { eventDispatcher } from '@deepkit/event'; + +class MyWebsite { + @http.GET('/') + open() { + return 'Welcome'; + } + + @http.GET('/admin').group('secret') + secret() { + return 'Welcome to the dark side'; + } +} + +const app = new App({ + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + +app.listen(httpWorkflow.onController, async (event) => { + if (event.route.groups.includes('secret')) { + //여기에서 cookie 세션, JWT 등 인증 정보를 확인합니다. + + //'accessDenied' 워크플로 상태로 점프하여 + // 사실상 모든 onAccessDenied 리스너를 실행합니다. + + //우리 리스너가 HTTP 커널의 리스너보다 먼저 호출되므로 + // 표준 컨트롤러 액션은 절대 호출되지 않습니다. + //내부적으로 event.next('accessDenied', ...)를 호출합니다 + event.accessDenied(); + } +}); + +/** + * 기본 accessDenied 구현을 변경합니다. + */ +app.listen(httpWorkflow.onAccessDenied, async () => { + if (event.sent) return; + if (event.hasNext()) return; + event.send(new HtmlResponse('No access to this area.', 403)); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Welcome +$ curl http://localhost:8080/admin +No access to this area +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http/getting-started.md b/website/src/translations/ko/documentation/http/getting-started.md new file mode 100644 index 000000000..35c946280 --- /dev/null +++ b/website/src/translations/ko/documentation/http/getting-started.md @@ -0,0 +1,210 @@ +# 시작하기 + +Deepkit HTTP는 Runtime Types에 기반하므로, 먼저 Runtime Types가 올바르게 설치되어 있어야 합니다. [Runtime Types 설치](../runtime-types/getting-started.md)를 참고하세요. + +이 작업이 성공적으로 완료되면, 라이브러리를 내부적으로 사용하는 Deepkit framework를 사용하거나 `@deepkit/app`을 설치할 수 있습니다. + +```sh +npm install @deepkit/http +``` + +controller API용 `@deepkit/http`는 TypeScript annotations에 기반하므로 controller API를 사용할 때 `experimentalDecorators` 기능을 활성화해야 합니다. +클래스를 사용하지 않는다면 이 기능을 활성화할 필요가 없습니다. + +_파일: tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +라이브러리를 설치하면 해당 API를 바로 사용할 수 있습니다. + +## Functional API + +Functional API는 함수에 기반하며, 앱의 DI 컨테이너를 통해 얻을 수 있는 router registry를 통해 등록할 수 있습니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule] +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return "Hello World!"; +}); + +app.run(); +``` + +모듈을 사용하면, 함수형 라우트도 모듈에서 동적으로 제공할 수 있습니다. + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +class MyModule extends createModuleClass({}) { + override process() { + this.configureProvider<HttpRouterRegistry>(router => { + router.get('/', () => { + return "Hello World!"; + }); + }); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +자세한 내용은 [프레임워크 모듈](../app/modules)을 참고하여 App 모듈에 대해 알아보세요. + +## Controller API + +Controller API는 클래스에 기반하며 App-API의 `controllers` 옵션을 통해 등록할 수 있습니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +new App({ + controllers: [MyPage], + imports: [new FrameworkModule] +}).run(); +``` + +모듈을 사용하면 컨트롤러도 모듈에서 제공할 수 있습니다. + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +class MyModule extends createModuleClass({}) { + override process() { + this.addController(MyPage); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +컨트롤러를 동적으로 제공하려면(예: 설정 옵션에 따라) `process` hook을 사용할 수 있습니다. + +```typescript +class MyModuleConfiguration { + debug: boolean = false; +} + +class MyModule extends createModuleClass({ + config: MyModuleConfiguration +}) { + override process() { + if (this.config.debug) { + class DebugController { + @http.GET('/debug/') + root() { + return 'Hello Debugger'; + } + } + this.addController(DebugController); + } + } +} +``` + +자세한 내용은 [프레임워크 모듈](../app/modules)을 참고하여 App 모듈에 대해 알아보세요. + +## HTTP 서버 + +Deepkit Framework를 사용하면 HTTP 서버가 이미 내장되어 있습니다. 그러나 Deepkit Framework를 사용하지 않고도 HTTP 라이브러리를 자체 HTTP 서버와 함께 사용할 수 있습니다. + +```typescript +import { Server } from 'http'; +import { HttpRequest, HttpResponse } from '@deepkit/http'; + +const app = new App({ + controllers: [MyPage], + imports: [new HttpModule] +}); + +const httpKernel = app.get(HttpKernel); + +new Server( + { IncomingMessage: HttpRequest, ServerResponse: HttpResponse, }, + ((req, res) => { + httpKernel.handleRequest(req as HttpRequest, res as HttpResponse); + }) +).listen(8080, () => { + console.log('listen at 8080'); +}); +``` + +## HTTP 클라이언트 + +todo: fetch API, validation, und cast. + +## 라우트 이름 + +라우트에 고유한 이름을 지정하고 forwarding 시 이를 참조할 수 있습니다. 사용하는 API에 따라 이름을 정의하는 방식이 다릅니다. + +```typescript +//functional API +router.get({ + path: '/user/:id', + name: 'userDetail' +}, (id: number) => { + return {userId: id}; +}); + +//controller API +class UserController { + @http.GET('/user/:id').name('userDetail') + userDetail(id: number) { + return {userId: id}; + } +} +``` + +이름이 있는 모든 라우트에 대해 `Router.resolveUrl()`로 URL을 요청할 수 있습니다. + +```typescript +import { HttpRouter } from '@deepkit/http'; +const router = app.get(HttpRouter); +router.resolveUrl('userDetail', {id: 2}); //=> '/user/2' +``` + +## 보안 + +## 세션 \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http/input-output.md b/website/src/translations/ko/documentation/http/input-output.md new file mode 100644 index 000000000..c9a2bdb69 --- /dev/null +++ b/website/src/translations/ko/documentation/http/input-output.md @@ -0,0 +1,484 @@ +# 입력 & 출력 + +HTTP route의 입력과 출력은 서버로 전송되는 데이터와 클라이언트로 다시 전송되는 데이터를 말합니다. 여기에는 path parameters, query parameters, body, headers, 그리고 response 자체가 포함됩니다. 이 장에서는 HTTP route에서 데이터를 읽고, deserialize하고, validate하고, 쓰는 방법을 살펴봅니다. + +## 입력 + +아래의 모든 입력 방식은 functional API와 controller API 모두에서 동일하게 동작합니다. 이들은 HTTP 요청에서 데이터를 typesafe하고 decoupled한 방식으로 읽을 수 있게 합니다. 이는 보안이 크게 향상될 뿐 아니라, 엄밀히 말해 route를 테스트하기 위해 HTTP request 객체조차 필요하지 않기 때문에 유닛 테스트도 단순화됩니다. + +모든 parameters는 정의된 TypeScript Type으로 자동 변환(deserialize)되고 validate됩니다. 이는 Deepkit Runtime Types와 그 [Serialization](../runtime-types/serialization.md) 및 [Validation](../runtime-types/validation) 기능을 통해 수행됩니다. + +간단히 하기 위해, 아래에는 functional API 예시만 보여줍니다. + +### Path Parameters + +Path parameters는 route의 URL에서 추출된 값입니다. 값의 타입은 Function 또는 Method의 해당 Parameter의 타입에 따라 결정됩니다. 변환은 [Soft Type Conversion](../runtime-types/serialization#soft-type-conversion) 기능으로 자동으로 수행됩니다. + +```typescript +router.get('/:text', (text: string) => { + return 'Hello ' + text; +}); +``` + +```sh +$ curl http://localhost:8080/galaxy +Hello galaxy +``` + +Path parameter가 string 이외의 Type으로 정의되면 올바르게 변환됩니다. + +```typescript +router.get('/user/:id', (id: number) => { + return `${id} ${typeof id}`; +}); +``` + +```sh +$ curl http://localhost:8080/user/23 +23 number +``` + +추가적인 validation constraints도 타입에 적용할 수 있습니다. + +```typescript +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: number & Positive) => { + return `${id} ${typeof id}`; +}); +``` + +`@deepkit/type`의 모든 validation types를 적용할 수 있습니다. 이에 대한 자세한 내용은 [HTTP Validation](#validation)을 참고하세요. + +Path parameters는 URL 매칭 시 기본적으로 정규식 `[^]+`가 설정됩니다. 이 RegExp는 다음과 같이 사용자 정의할 수 있습니다: + +```typescript +import { HttpRegExp } from '@deepkit/http'; +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: HttpRegExp<number & Positive, '[0-9]+'>) => { + return `${id} ${typeof id}`; +}); +``` + +이는 예외적인 경우에만 필요합니다. 대부분 Type과 validation types의 조합만으로도 가능한 값을 이미 올바르게 제한하기 때문입니다. + +### Query Parameters + +Query parameters는 URL에서 `?` 문자 이후의 값이며 `HttpQuery<T>` 타입으로 읽을 수 있습니다. Parameter의 이름은 query parameter의 이름과 동일합니다. + +```typescript +import { HttpQuery } from '@deepkit/http'; + +router.get('/', (text: HttpQuery<number>) => { + return `Hello ${text}`; +}); +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +``` + +Query parameters도 자동으로 deserialize되고 validate됩니다. + +```typescript +import { HttpQuery } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/', (text: HttpQuery<string> & MinLength<3>) => { + return 'Hello ' + text; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +$ curl http://localhost:8080/\?text\=ga +error +``` + +`@deepkit/type`의 모든 validation types를 적용할 수 있습니다. 이에 대한 자세한 내용은 [HTTP Validation](#validation)을 참고하세요. + +경고: Parameter 값은 escape/sanitize되지 않습니다. 이를 HTML로 route에서 문자열에 직접 반환하면 보안 취약점(XSS)이 발생합니다. 외부 입력은 절대 신뢰하지 말고 필요한 곳에서 filter/sanitize/convert 하세요. + +### Query Model + +Query parameters가 많아지면 쉽게 혼란스러워질 수 있습니다. 이를 정리하기 위해 모든 가능한 query parameters를 요약하는 model(Class 또는 Interface)을 사용할 수 있습니다. + +```typescript +import { HttpQueries } from '@deepkit/http'; + +class HelloWorldQuery { + text!: string; + page: number = 0; +} + +router.get('/', (query: HttpQueries<HelloWorldQuery>) +{ + return 'Hello ' + query.text + ' at page ' + query.page; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy&page=1 +Hello galaxy at page 1 +``` + +지정된 model의 Properties는 `@deepkit/type`가 지원하는 모든 TypeScript Types 및 validation types를 포함할 수 있습니다. [Serialization](../runtime-types/serialization.md) 및 [Validation](../runtime-types/validation.md) 챕터를 참고하세요. + +### Body + +HTTP body를 허용하는 HTTP Method의 경우, body model도 지정할 수 있습니다. HTTP 요청의 body content type은 Deepkit이 이를 JavaScript 객체로 자동 변환할 수 있도록 `application/x-www-form-urlencoded`, `multipart/form-data` 또는 `application/json`이어야 합니다. + +```typescript +import { HttpBody } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBody<HelloWorldBody>) => { + return 'Hello ' + body.text; +} +``` + +### Header + +### Stream + +### Validation 수동 처리 + +Body model의 validation을 수동으로 처리하려면 특수 타입 `HttpBodyValidation<T>`를 사용할 수 있습니다. 이를 통해 유효하지 않은 body 데이터도 수신하고 에러 메시지에 매우 구체적으로 대응할 수 있습니다. + +```typescript +import { HttpBodyValidation } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBodyValidation<HelloWorldBody>) => { + if (!body.valid()) { + // 휴스턴, 문제가 생겼습니다. + const textError = body.getErrorMessageForPath('text'); + return 'Text is invalid, please fix it. ' + textError; + } + + return 'Hello ' + body.text; +}) +``` + +`valid()`가 `false`를 반환하는 즉시 지정된 model의 값들은 오류 상태일 수 있습니다. 이는 validation이 실패했음을 의미합니다. `HttpBodyValidation`이 사용되지 않고 잘못된 HTTP 요청이 수신되면, 요청은 즉시 중단되고 Function 내부의 코드는 실행되지 않습니다. 예를 들어 body에 대한 에러 메시지를 동일한 route에서 수동으로 처리해야 하는 경우에만 `HttpBodyValidation`을 사용하세요. + +지정된 model의 Properties는 `@deepkit/type`가 지원하는 모든 TypeScript Types 및 validation types를 포함할 수 있습니다. [Serialization](../runtime-types/serialization.md) 및 [Validation](../runtime-types/validation.md) 챕터를 참고하세요. + +### 파일 업로드 + +클라이언트가 파일을 업로드할 수 있도록 body model에 특수 Property Type을 사용할 수 있습니다. `UploadedFile`은 원하는 만큼 사용할 수 있습니다. + +```typescript +import { UploadedFile, HttpBody } from '@deepkit/http'; +import { readFileSync } from 'fs'; + +class HelloWordBody { + file!: UploadedFile; +} + +router.post('/', (body: HttpBody<HelloWordBody>) => { + const content = readFileSync(body.file.path); + + return { + uploadedFile: body.file + }; +}) +``` + +```sh +$ curl http://localhost:8080/ -X POST -H "Content-Type: multipart/form-data" -F "file=@Downloads/23931.png" +{ + "uploadedFile": { + "size":6430, + "path":"/var/folders/pn/40jxd3dj0fg957gqv_nhz5dw0000gn/T/upload_dd0c7241133326bf6afddc233e34affa", + "name":"23931.png", + "type":"image/png", + "lastModifiedDate":"2021-06-11T19:19:14.775Z" + } +} +``` + +기본적으로 Router는 업로드된 모든 파일을 temp 폴더에 저장하고 route의 코드가 실행된 후 제거합니다. 따라서 `path`에 지정된 경로에서 파일을 읽어 영구 위치(로컬 디스크, 클라우드 스토리지, 데이터베이스)에 저장해야 합니다. + +## Validation + +HTTP 서버에서 Validation은 필수 기능입니다. 거의 항상 신뢰할 수 없는 데이터와 작업하기 때문입니다. 데이터가 여러 곳에서 validate될수록 서버는 더 안정적입니다. HTTP routes에서 Validation은 types와 validation constraints를 통해 편리하게 사용할 수 있으며, `@deepkit/type`의 고도로 최적화된 validator로 검증되므로 성능 문제는 없습니다. 따라서 이러한 validation 기능을 적극 사용하는 것이 매우 권장됩니다. 과유불급보다는 차라리 한 번 더 Validate하는 편이 낫습니다. + +path parameters, query parameters, body parameters 등 모든 입력은 지정된 TypeScript Type에 대해 자동으로 validate됩니다. 추가 constraints가 `@deepkit/type`의 types를 통해 지정되면, 이들도 함께 검사됩니다. + +```typescript +import { HttpQuery, HttpQueries, HttpBody } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/:text', (text: string & MinLength<3>) => { + return 'Hello ' + text; +} + +router.get('/', (text: HttpQuery<string> & MinLength<3>) => { + return 'Hello ' + text; +} + +interface MyQuery { + text: string & MinLength<3>; +} + +router.get('/', (query: HttpQueries<MyQuery>) => { + return 'Hello ' + query.text; +}); + +router.post('/', (body: HttpBody<MyQuery>) => { + return 'Hello ' + body.text; +}); +``` + +자세한 내용은 [Validation](../runtime-types/validation.md)을 참고하세요. + +## 출력 + +route는 다양한 데이터 구조를 반환할 수 있습니다. 리다이렉트와 템플릿과 같이 특별하게 처리되는 것도 있고, 단순 객체처럼 JSON으로 그대로 전송되는 것도 있습니다. + +### JSON + +기본적으로 일반 JavaScript 값은 `applicationjson; charset=utf-8` 헤더와 함께 JSON으로 클라이언트에 반환됩니다. + +```typescript +router.get('/', () => { + // application/json으로 전송됩니다 + return { hello: 'world' } +}); +``` + +Function 또는 Method에 명시적인 return type을 지정하면, 데이터는 해당 Type에 따라 Deepkit JSON Serializer로 JSON에 serialize됩니다. + +```typescript +interface ResultType { + hello: string; +} + +router.get('/', (): ResultType => { + // application/json으로 전송되며 additionalProperty는 제거됩니다 + return { hello: 'world', additionalProperty: 'value' }; +}); +``` + +### HTML + +HTML을 보내는 방법은 두 가지가 있습니다. `HtmlResponse` 객체를 사용하거나 JSX가 있는 Template Engine을 사용하는 것입니다. + +```typescript +import { HtmlResponse } from '@deepkit/http'; + +router.get('/', () => { + // Content-Type: text/html로 전송됩니다 + return new HtmlResponse('<b>Hello World</b>'); +}); +``` + +```typescript +router.get('/', () => { + // Content-Type: text/html로 전송됩니다 + return <b>Hello + World < /b>; +}); +``` + +JSX가 있는 템플릿 엔진 방식은 사용된 변수가 자동으로 HTML escape된다는 장점이 있습니다. [Template](./template.md)도 참고하세요. + +### 사용자 지정 Content Type + +HTML과 JSON 외에도 특정 content type으로 텍스트 또는 바이너리 데이터를 보낼 수 있습니다. 이는 `Response` 객체를 통해 수행됩니다. + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('<title>Hello World', 'text/xml'); +}); +``` + +### HTTP Errors + +여러 HTTP errors를 throw하여 HTTP 요청의 처리를 즉시 중단하고 해당 error의 HTTP status를 출력할 수 있습니다. + +```typescript +import { HttpNotFoundError } from '@deepkit/http'; + +router.get('/user/:id', async (id: number, database: Database) => { + const user = await database.query(User).filter({ id }).findOneOrUndefined(); + if (!user) throw new HttpNotFoundError('User not found'); + return user; +}); +``` + +기본적으로 모든 errors는 JSON으로 클라이언트에 반환됩니다. 이 동작은 이벤트 시스템의 `httpWorkflow.onControllerError` 이벤트에서 사용자 정의할 수 있습니다. [HTTP Events](./events.md) 섹션을 참고하세요. + +| Error class | Status | +|---------------------------|--------| +| HttpBadRequestError | 400 | +| HttpUnauthorizedError | 401 | +| HttpAccessDeniedError | 403 | +| HttpNotFoundError | 404 | +| HttpMethodNotAllowedError | 405 | +| HttpNotAcceptableError | 406 | +| HttpTimeoutError | 408 | +| HttpConflictError | 409 | +| HttpGoneError | 410 | +| HttpTooManyRequestsError | 429 | +| HttpInternalServerError | 500 | +| HttpNotImplementedError | 501 | + +`HttpAccessDeniedError`는 특별한 경우입니다. 이 에러가 throw되면 HTTP workflow(참고: [HTTP Events](./events.md))는 `controllerError`로 가지 않고 `accessDenied`로 점프합니다. + +`createHttpError`로 사용자 정의 HTTP errors를 만들고 throw할 수 있습니다. + +```typescript +export class HttpMyError extends createHttpError(412, 'My Error Message') { +} +``` + +controller action에서 throw된 errors는 HTTP workflow 이벤트 `onControllerError`에 의해 처리됩니다. 기본 구현은 에러 메시지와 상태 코드로 JSON response를 반환하는 것입니다. 이 이벤트를 구독해 다른 response를 반환하도록 사용자 정의할 수 있습니다. + +```typescript +import { httpWorkflow } from '@deepkit/http'; + +new App() + .listen(httpWorkflow.onControllerError, (event) => { + if (event.error instanceof HttpMyError) { + event.send(new Response('My Error Message', 'text/plain').status(500)); + } else { + // 다른 모든 에러에 대해서는 일반적인 에러 메시지를 반환합니다 + event.send(new Response('Something went wrong. Sorry about that.', 'text/plain').status(500)); + } + }) + .listen(httpWorkflow.onAccessDenied, (event) => { + event.send(new Response('Access denied. Try to login first.', 'text/plain').status(403)); + }); +``` + +### 추가 헤더 + +HTTP response의 header를 수정하려면 `Response`, `JSONResponse`, `HTMLResponse` 객체에서 추가 Method를 호출할 수 있습니다. + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('Access Denied', 'text/plain') + .header('X-Reason', 'unknown') + .status(403); +}); +``` + +### Redirect + +301 또는 302 redirect를 response로 반환하려면 `Redirect.toRoute` 또는 `Redirect.toUrl`을 사용할 수 있습니다. + +```typescript +import { Redirect } from '@deepkit/http'; + +router.get({ path: '/', name: 'homepage' }, () => { + return Hello + World < /b>; +}); + +router.get({ path: '/registration/complete' }, () => { + return Redirect.toRoute('homepage'); +}); +``` + +`Redirect.toRoute` Method는 여기서 route name을 사용합니다. route name을 설정하는 방법은 [HTTP Route Names](./getting-started.md#route-names) 섹션을 참고하세요. 이 참조된 route(query 또는 path)에 parameters가 포함되어 있으면 두 번째 인자를 통해 지정할 수 있습니다: + +```typescript +router.get({ path: '/user/:id', name: 'user_detail' }, (id: number) => { + +}); + +router.post('/user', (user: HttpBody) => { + //... user를 저장하고 상세 페이지로 redirect + return Redirect.toRoute('user_detail', { id: 23 }); +}); +``` + +또는 `Redirect.toUrl`로 URL로 redirect할 수 있습니다. + +```typescript +router.post('/user', (user: HttpBody) => { + //... user를 저장하고 상세 페이지로 redirect + return Redirect.toUrl('/user/' + 23); +}); +``` + +기본적으로 둘 다 302 forwarding을 사용합니다. 이는 `statusCode` Argument로 사용자 정의할 수 있습니다. + +## Resolver + +Router는 복잡한 parameter types를 resolve하는 방법을 지원합니다. 예를 들어 `/user/:id`와 같은 route가 주어졌을 때, 이 `id`를 resolver를 사용해 route 외부에서 `user` 객체로 resolve할 수 있습니다. 이는 HTTP 추상화와 route 코드를 더욱 분리하여 테스트와 모듈화를 더 단순화합니다. + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http, RouteParameterResolverContext, RouteParameterResolver } from '@deepkit/http'; + +class UserResolver implements RouteParameterResolver { + constructor(protected database: Database) { + } + + async resolve(context: RouteParameterResolverContext) { + if (!context.parameters.id) throw new Error('No :id given'); + return await this.database.getUser(parseInt(context.parameters.id, 10)); + } +} + +@http.resolveParameter(User, UserResolver) +class MyWebsite { + @http.GET('/user/:id') + getUser(user: User) { + return 'Hello ' + user.username; + } +} + +new App({ + controllers: [MyWebsite], + providers: [UserDatabase, UserResolver], + imports: [new FrameworkModule] +}) + .run(); +``` + +`@http.resolveParameter`의 데코레이터는 어떤 Class가 `UserResolver`로 resolve되어야 하는지 지정합니다. 지정된 Class `User`가 Function 또는 Method의 Parameter로 지정되는 즉시, resolver가 이를 제공하는 데 사용됩니다. + +`@http.resolveParameter`가 Class에 지정되면 이 Class의 모든 Method가 해당 resolver를 갖습니다. 데코레이터는 Method별로도 적용할 수 있습니다: + +```typescript +class MyWebsite { + @http.GET('/user/:id').resolveParameter(User, UserResolver) + getUser(user: User) { + return 'Hello ' + user.username; + } +} +``` + +또한 functional API도 사용할 수 있습니다: + +```typescript + +router.add( + http.GET('/user/:id').resolveParameter(User, UserResolver), + (user: User) => { + return 'Hello ' + user.username; + } +); +``` + +`User` 객체는 반드시 parameter에 의존할 필요는 없습니다. session이나 HTTP header에 의존할 수도 있으며, 사용자가 로그인했을 때만 제공되도록 할 수도 있습니다. `RouteParameterResolverContext`에는 HTTP 요청에 대한 많은 정보가 제공되어 다양한 사용 사례를 매핑할 수 있습니다. + +원칙적으로는 `http` scope의 Dependency Injection 컨테이너를 통해 복잡한 parameter types를 제공하도록 하는 것도 가능합니다. 이는 route Function 또는 Method에서도 사용할 수 있습니다. 그러나 DI 컨테이너는 전체적으로 동기식이므로 비동기 Function 호출을 사용할 수 없다는 단점이 있습니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http/middleware.md b/website/src/translations/ko/documentation/http/middleware.md new file mode 100644 index 000000000..ab3add8b9 --- /dev/null +++ b/website/src/translations/ko/documentation/http/middleware.md @@ -0,0 +1,261 @@ +# 미들웨어 + +HTTP 미들웨어는 HTTP events의 대안으로 요청/응답 사이클에 hook할 수 있게 해줍니다. 해당 API는 Express/Connect 프레임워크의 모든 미들웨어를 사용할 수 있도록 합니다. + +미들웨어는 의존성 주입 컨테이너에 의해 인스턴스화되는 Class이거나, 간단한 Function일 수 있습니다. + +```typescript +import { HttpMiddleware, httpMiddleware, HttpRequest, HttpResponse } from '@deepkit/http'; + +class MyMiddleware implements HttpMiddleware { + async execute(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); + } +} + + +function myMiddlewareFunction(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); +} + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware), + httpMiddleware.for(myMiddlewareFunction), + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 전역 + +`httpMiddleware.for(MyMiddleware)`를 사용하면 미들웨어가 모든 라우트에 전역으로 등록됩니다. + +```typescript +import { httpMiddleware } from '@deepkit/http'; + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 컨트롤러 단위 + +미들웨어를 하나 또는 여러 컨트롤러에만 제한하는 두 가지 방법이 있습니다. `@http.controller`를 사용하거나 `httpMiddleware.for(T).forControllers()`를 사용할 수 있습니다. `excludeControllers`를 사용하면 특정 컨트롤러를 제외할 수 있습니다. + +```typescript +@http.middleware(MyMiddleware) +class MyFirstController { + +} +new App({ + providers: [MyMiddleware], + controllers: [MainController, UsersCommand], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyFirstController, MySecondController) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 라우트 이름 단위 + +`forRouteNames`와 이에 대응하는 `excludeRouteNames`를 사용하면 라우트 이름별로 미들웨어 실행을 필터링할 수 있습니다. + +```typescript +class MyFirstController { + @http.GET('/hello').name('firstRoute') + myAction() { + } + + @http.GET('/second').name('secondRoute') + myAction2() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRouteNames('firstRoute', 'secondRoute') + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 액션/라우트 단위 + +특정 라우트에만 미들웨어를 실행하려면 `@http.GET().middleware()` 또는 +`httpMiddleware.for(T).forRoute()`를 사용할 수 있으며, forRoute에는 라우트를 필터링하기 위한 여러 옵션이 있습니다. + +```typescript +class MyFirstController { + @http.GET('/hello').middleware(MyMiddleware) + myAction() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' + }) + ], + imports: [new FrameworkModule] +}).run(); +``` + +`forRoutes()`의 첫 번째 인수로 라우트를 필터링하는 여러 방법을 지정할 수 있습니다. + +```typescript +{ + path?: string; + pathRegExp?: RegExp; + httpMethod?: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS' | 'TRACE'; + category?: string; + excludeCategory?: string; + group?: string; + excludeGroup?: string; +} +``` + +## 경로 패턴 + +`path`는 와일드카드 *를 지원합니다. + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' +}) +``` + +## 정규식 + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + pathRegExp: /'api/.*'/ +}) +``` + +## HTTP 메서드 + +모든 라우트를 특정 HTTP 메서드로 필터링합니다. + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + httpMethod: 'GET' +}) +``` + +## 카테고리 + +`category`와 이에 대응하는 `excludeCategory`를 사용하면 라우트 카테고리별로 필터링할 수 있습니다. + +```typescript +@http.category('myCategory') +class MyFirstController { + +} + +class MySecondController { + @http.GET().category('myCategory') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + category: 'myCategory' +}) +``` + +## 그룹 + +`group`과 이에 대응하는 `excludeGroup`을 사용하면 라우트 그룹별로 필터링할 수 있습니다. + +```typescript +@http.group('myGroup') +class MyFirstController { + +} + +class MySecondController { + @http.GET().group('myGroup') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + group: 'myGroup' +}) +``` + +## 모듈 단위 + +모듈 전체에 대해 미들웨어 적용을 제한할 수 있습니다. + +```typescript +httpMiddleware.for(MyMiddleware).forModule(ApiModule) +``` + +## 자체 모듈 단위 + +미들웨어가 등록된 모듈의 모든 컨트롤러/라우트에 미들웨어를 적용하려면 `forSelfModules()`를 사용하세요. + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //같은 모듈에 등록된 모든 컨트롤러에 대해 + httpMiddleware.for(MyMiddleware).forSelfModules(), + ], +}); +``` + +## 타임아웃 + +모든 미들웨어는 결국 `next()`를 호출해야 합니다. 미들웨어가 타임아웃 내에 `next()`를 호출하지 않으면 경고가 로깅되고 다음 미들웨어가 실행됩니다. 기본값인 4초를 다른 값으로 변경하려면 timeout(milliseconds)를 사용하세요. + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //같은 모듈에 등록된 모든 컨트롤러에 대해 + httpMiddleware.for(MyMiddleware).timeout(15_000), + ], +}); +``` + +## 다중 규칙 + +여러 필터를 결합하려면 메서드 호출을 체이닝하면 됩니다. + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MyController], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyController).excludeRouteNames('secondRoute') + ], +}); +``` + +## Express 미들웨어 + +거의 모든 express 미들웨어가 지원됩니다. 다만 express의 특정 request 메서드에 접근하는 미들웨어는 아직 지원되지 않습니다. + +```typescript +import * as compression from 'compression'; + +const ApiModule = new AppModule({}, { + middlewares: [ + httpMiddleware.for(compress()).forControllers(MyController) + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http/security.md b/website/src/translations/ko/documentation/http/security.md new file mode 100644 index 000000000..8bb7a6a9e --- /dev/null +++ b/website/src/translations/ko/documentation/http/security.md @@ -0,0 +1,3 @@ +# 보안 + +이 섹션은 아직 작성 중입니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/http/views.md b/website/src/translations/ko/documentation/http/views.md new file mode 100644 index 000000000..850ec37e2 --- /dev/null +++ b/website/src/translations/ko/documentation/http/views.md @@ -0,0 +1,31 @@ +# HTML 뷰 + +Deepkit HTTP에는 내장 HTML 뷰 렌더링 시스템이 포함되어 있습니다. 이는 JSX 기반이며, 뷰를 TypeScript로 작성할 수 있게 해줍니다. 자체 문법을 가진 템플릿 엔진이 아니라, 완전한 TypeScript/JSX 렌더러입니다. + +런타임에 JSX 코드를 최적화하고 결과를 캐시합니다. 따라서 매우 빠르며 오버헤드가 거의 없습니다. + + +## JSX + +JSX는 JavaScript의 문법 확장이며, 기본적으로 TypeScript 지원을 제공합니다. TypeScript에서 HTML을 작성할 수 있게 해줍니다. Vue.js 또는 React.js와 매우 유사합니다. + +```tsx app=app.ts +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from "@deepkit/http"; + +export function View() { + return
+

Hello World

+

My first JSX view

+
; +} + +const app = new App({}); +const router = app.get(HttpRouterRegistry); + +router.get('/', () => ); + +app.run(); +``` + +```sh \ No newline at end of file diff --git a/website/src/translations/ko/documentation/index.md b/website/src/translations/ko/documentation/index.md new file mode 100644 index 000000000..8a1e2460f --- /dev/null +++ b/website/src/translations/ko/documentation/index.md @@ -0,0 +1,87 @@ +# 문서 + +Deepkit은 MIT 라이선스 하에 자유롭게 제공되는 오픈 소스 TypeScript 프레임워크로, 확장 가능하고 유지보수가 쉬운 백엔드 애플리케이션을 구축하도록 설계되었습니다. 브라우저와 Node.js에서 동작하도록 설계되었지만, 적합한 어느 JavaScript 환경에서도 실행될 수 있습니다. + +여기에서 Deepkit의 다양한 구성 요소에 대한 챕터와 모든 패키지의 API 레퍼런스를 확인할 수 있습니다. + +도움이 필요하시면 언제든지 우리의 [Discord 서버](https://discord.com/invite/PtfVf7B8UU)에 참여하시거나 [GitHub](https://github.com/deepkit/deepkit-framework)에서 이슈를 열어주세요. + +## 챕터 + + +- [앱](/documentation/app.md) - 명령줄 인터페이스를 기반으로 Deepkit으로 첫 애플리케이션을 작성합니다. +- [프레임워크](/documentation/framework.md) - 애플리케이션에 (HTTP/RPC) 서버, API 문서, 디버거, 통합 테스트 등을 추가합니다. +- [런타임 타입](/documentation/runtime-types.md) - TypeScript 런타임 타입과 데이터 검증 및 변환에 대해 학습합니다. +- [의존성 주입](/documentation/dependency-injection.md) - 의존성 주입 컨테이너, 제어의 역전, 의존성 역전. +- [파일시스템](/documentation/filesystem.md) - 로컬 및 원격 파일 시스템을 통합된 방식으로 다루기 위한 파일시스템 추상화. +- [브로커](/documentation/broker.md) - 분산 L2 캐시, pub/sub, 큐, 중앙 원자적 락, 키-값 저장소와 함께 작업하기 위한 메시지 브로커 추상화. +- [HTTP](/documentation/http.md) - 타입 안전한 엔드포인트를 구축하기 위한 HTTP 서버 추상화. +- [RPC](/documentation/rpc.md) - 프론트엔드를 백엔드와 연결하거나 여러 백엔드 서비스를 연결하기 위한 원격 프로시저 호출(RPC) 추상화. +- [ORM](/documentation/orm.md) - 데이터를 타입 안전한 방식으로 저장하고 질의하기 위한 ORM 및 DBAL. +- [데스크톱 UI](/documentation/desktop-ui/getting-started) - Deepkit의 Angular 기반 UI 프레임워크로 GUI 애플리케이션을 빌드합니다. + +## API 레퍼런스 + +다음은 모든 Deepkit 패키지와 그들의 API 문서 링크의 전체 목록입니다. + +### 구성 + +- [@deepkit/app](/documentation/package/app.md) +- [@deepkit/framework](/documentation/package/framework.md) +- [@deepkit/http](/documentation/package/http.md) +- [@deepkit/angular-ssr](/documentation/package/angular-ssr.md) + +### 인프라 + +- [@deepkit/rpc](/documentation/package/rpc.md) +- [@deepkit/rpc-tcp](/documentation/package/rpc-tcp.md) +- [@deepkit/broker](/documentation/package/broker.md) +- [@deepkit/broker-redis](/documentation/package/broker-redis.md) + +### 파일시스템 + +- [@deepkit/filesystem](/documentation/package/filesystem.md) +- [@deepkit/filesystem-ftp](/documentation/package/filesystem-ftp.md) +- [@deepkit/filesystem-sftp](/documentation/package/filesystem-sftp.md) +- [@deepkit/filesystem-s3](/documentation/package/filesystem-s3.md) +- [@deepkit/filesystem-google](/documentation/package/filesystem-google.md) +- [@deepkit/filesystem-database](/documentation/package/filesystem-database.md) + +### 데이터베이스 + +- [@deepkit/orm](/documentation/package/orm.md) +- [@deepkit/mysql](/documentation/package/mysql.md) +- [@deepkit/postgres](/documentation/package/postgres.md) +- [@deepkit/sqlite](/documentation/package/sqlite.md) +- [@deepkit/mongodb](/documentation/package/mongodb.md) + +### 기초 + +- [@deepkit/type](/documentation/package/type.md) +- [@deepkit/event](/documentation/package/event.md) +- [@deepkit/injector](/documentation/package/injector.md) +- [@deepkit/template](/documentation/package/template.md) +- [@deepkit/logger](/documentation/package/logger.md) +- [@deepkit/workflow](/documentation/package/workflow.md) +- [@deepkit/stopwatch](/documentation/package/stopwatch.md) + +### 도구 + +- [@deepkit/api-console](/documentation/package/api-console.md) +- [@deepkit/devtool](/documentation/package/devtool.md) +- [@deepkit/desktop-ui](/documentation/package/desktop-ui.md) +- [@deepkit/orm-browser](/documentation/package/orm-browser.md) +- [@deepkit/bench](/documentation/package/bench.md) +- [@deepkit/run](/documentation/package/run.md) + +### 핵심 + +- [@deepkit/bson](/documentation/package/bson.md) +- [@deepkit/core](/documentation/package/core.md) +- [@deepkit/topsort](/documentation/package/topsort.md) + +### 런타임 + +- [@deepkit/vite](/documentation/package/vite.md) +- [@deepkit/bun](/documentation/package/bun.md) +- [@deepkit/type-compiler](/documentation/package/type-compiler.md) \ No newline at end of file diff --git a/website/src/translations/ko/documentation/introduction.md b/website/src/translations/ko/documentation/introduction.md new file mode 100644 index 000000000..33debb53d --- /dev/null +++ b/website/src/translations/ko/documentation/introduction.md @@ -0,0 +1,48 @@ +# 소개 + +TypeScript는 더 안전하고 견고한 애플리케이션 개발을 위해 설계된, JavaScript의 고도로 확장 가능한 상위 집합으로 자리매김했습니다. JavaScript가 방대한 개발자 커뮤니티와 생태계를 구축해 왔지만, TypeScript는 정적 타이핑의 힘을 JavaScript에 가져와 런타임 오류를 크게 줄이고 코드베이스의 유지보수성과 이해도를 높입니다. 그러나 이러한 장점에도 불구하고, 특히 복잡한 엔터프라이즈급 솔루션을 구현하는 데 있어 TypeScript의 잠재력은 아직 충분히 활용되지 못했습니다. TypeScript는 본질적으로 컴파일 시점에 타입 정보를 제거하기 때문에, 런타임 기능에 결정적인 공백이 생기고 타입 정보를 보존하기 위해 온갖 불편하고 비직관적인 우회 방법을 구현해야 합니다. 코드 생성, 제약된 데코레이터, 또는 Zod와 같이 복잡한 추론 단계를 가진 커스텀 타입 빌더 등 어떤 방식이든, 이러한 해결책들은 번거롭고 느리며 오류가 발생하기 쉽습니다. 그 결과 개발 속도가 느려질 뿐 아니라, 특히 대규모 팀과 복잡한 프로젝트에서 애플리케이션의 견고함도 떨어집니다. + +이때 Deepkit이 등장합니다. 이는 TypeScript를 활용해 정교하고 효율적인 소프트웨어 솔루션을 구축하는 방식을 혁신하는 프레임워크입니다. TypeScript로, 그리고 TypeScript를 위해 설계된 Deepkit은 개발 중 타입 안정성을 보장할 뿐 아니라 TypeScript의 타입 시스템 이점을 런타임까지 확장합니다. 런타임에 타입 정보를 보존함으로써 Deepkit은 동적 타입 계산, 데이터 검증, 직렬화 등 기존에는 구현이 번거로웠던 수많은 새로운 기능의 문을 엽니다. + +Deepkit은 높은 복잡도의 프로젝트와 엔터프라이즈급 애플리케이션을 겨냥해 설계되었지만, 민첩성과 모듈식 아키텍처 덕분에 소규모 애플리케이션에도 똑같이 적합합니다. 방대한 라이브러리는 일반적인 사용 사례를 포괄하며, 프로젝트 필요에 따라 개별적으로 또는 통합해 사용할 수 있습니다. Deepkit은 필요할 만큼 유연하고 요구되는 만큼 구조적이 되는 것을 목표로 하여, 개발자가 단기와 장기 모두에서 높은 개발 속도를 유지할 수 있게 합니다. + +## 왜 Deepkit인가? + +TypeScript 생태계에는 수많은 라이브러리와 도구가 존재하여 거의 모든 상상 가능한 문제에 대한 해법을 제공합니다. 이처럼 선택지가 풍부한 것은 힘이 되지만, 서로 다른 라이브러리 간의 철학, API, 코드 품질 불일치로 인해 복잡성이 커지는 경우가 잦습니다. 이 이질적인 구성 요소들을 통합하려면 추가적인 추상화가 필요하며, 흔히 접착(glue) 코드가 대거 생겨나 유지보수의 악몽이 되고 금세 통제를 벗어나 개발 속도를 크게 떨어뜨립니다. Deepkit은 사실상 모든 프로젝트에 필요한 핵심 기능을 하나로 모은 통합 프레임워크를 제공함으로써 이러한 문제를 완화하고자 합니다. 조화롭게 설계된 라이브러리와 컴포넌트들은 서로 매끄럽게 동작하도록 만들어져, 분절된 TypeScript 생태계에 존재하는 간극을 메웁니다. + +### 검증된 엔터프라이즈 원칙 + +Deepkit은 Java의 Spring, PHP의 Laravel과 Symfony 같은 잘 정립된 엔터프라이즈 프레임워크에서 영감을 받았습니다. 이러한 프레임워크들은 수십 년에 걸쳐 효율성과 견고함을 입증하며 무수한 성공적인 프로젝트의 중추가 되어왔습니다. Deepkit은 유사한 엔터프라이즈 설계 패턴과 개념을 새로운 독창적 방식으로 TypeScript 세계에 도입해, 개발자가 오랜 집단적 지혜의 혜택을 누릴 수 있게 합니다. + +이러한 패턴은 애플리케이션을 구조화하는 검증된 방법을 제공할 뿐 아니라, 특히 대규모 팀에서 개발을 용이하게 합니다. 이처럼 검증된 방법론을 활용하여 Deepkit은 TypeScript 환경에서 찾기 어려웠던 수준의 신뢰성과 확장성을 제공하는 것을 목표로 합니다. + +### 애자일 / 장기적 성능 + +Deepkit은 민첩성을 염두에 두고 설계되어, 초기 개발을 가속하고 장기 유지보수에 이점을 주는 도구와 기능을 제공합니다. 초기 속도를 미래 확장성의 희생으로 삼는 일부 프레임워크와 달리, Deepkit은 둘 사이의 균형을 맞춥니다. 그 설계 패턴은 이해하기 쉬워 시작이 간단합니다. 동시에 뛰어난 확장성을 지녀, 프로젝트와 팀이 성장하더라도 개발 속도가 떨어지지 않도록 보장합니다. + +이러한 선제적 접근은 Deepkit을 빠른 MVP뿐 아니라 복잡하고 수명이 긴 엔터프라이즈 애플리케이션에도 이상적인 선택으로 만듭니다. + +### 개발자 경험 + +마지막으로, Deepkit은 개발자 경험을 강하게 강조합니다. 이 프레임워크는 직관적인 API, 상세한 문서, 그리고 지원적인 커뮤니티를 제공하여, 개발자가 기술적 복잡성과 씨름하기보다 비즈니스 문제 해결에 집중할 수 있도록 돕습니다. 소규모 애플리케이션을 만들든 대규모 엔터프라이즈급 시스템을 구축하든, Deepkit은 개발 여정을 원활하고 보람 있게 만들어 줄 도구와 모범 사례를 제공합니다. + +## 핵심 기능 + +### 런타임 타입 + +Deepkit의 두드러진 기능 중 하나는 런타임에 타입 정보를 보존하는 능력입니다. 기존 TypeScript 프레임워크는 컴파일 과정에서 이 중요한 데이터를 버리는 경우가 많아, 데이터 검증, 직렬화, 또는 의존성 주입 같은 런타임 작업이 훨씬 더 번거로워집니다. Deepkit의 타입 컴파일러는 런타임에서 동적 타입 계산과 기존 타입 정보의 읽기를 고유하게 가능하게 합니다. 이는 유연성을 높일 뿐 아니라 더 견고하고 타입 안전한 애플리케이션을 구현하게 하여, 복잡한 시스템 개발을 간소화합니다. + + +### 포괄적인 라이브러리 모음 + +Deepkit은 애플리케이션 개발의 다양한 측면을 가속하도록 설계된 완전한 라이브러리 생태계를 제공합니다. 데이터베이스 추상화와 CLI 파서부터 HTTP 라우터와 RPC 프레임워크에 이르기까지, Deepkit 라이브러리는 다양한 프로그래밍 요구를 충족하는 통합 솔루션을 제공합니다. 이러한 모든 라이브러리는 런타임에서 TypeScript’s 타입 시스템을 활용한다는 추가 이점을 지녀, 보일러플레이트를 크게 줄이고 코드 명료성을 높입니다. Deepkit의 모듈성 덕분에 개발자는 특정 작업에 개별 라이브러리를 사용하거나, 전체 프레임워크를 활용해 완전한 프로덕션 준비 애플리케이션을 구축할 수 있습니다. + +## 높은 성능과 확장성 + +프로젝트의 복잡도가 커질수록 개발 속도를 유지하는 것은 만만치 않은 도전입니다. Deepkit은 대규모 팀과 더 복잡한 코드베이스에서도 잘 확장되는 것으로 검증된 엔터프라이즈 설계 패턴의 적용에 초점을 맞춤으로써 이 문제를 정면으로 다룹니다. 이 프레임워크는 대규모 팀과 더 복잡한 코드베이스에서 잘 확장되는 것으로 입증된 엔터프라이즈 설계 패턴을 통합합니다. Deepkit의 접근 방식은 초기 단계뿐 아니라 전 생애주기에 걸쳐 프로젝트가 민첩하고 효율적으로 유지되도록 보장합니다. 이는 보일러플레이트를 최소화하고 설계 패턴을 가능한 가장 인체공학적인 방식으로 활용함으로써 달성되며, 팀이 장기간 높은 생산성을 유지할 수 있게 합니다. + + + +### 아이소모픽 TypeScript + +Deepkit은 아이소모픽 TypeScript의 이점을 극대화하도록 설계되었습니다. 이는 동일한 코드베이스를 프런트엔드, 백엔드, 심지어 모바일 애플리케이션 등 여러 플랫폼에서 사용할 수 있게 합니다. 그 결과 코드가 여러 부서 간에 공유될 수 있어 시간과 비용이 크게 절감되고, 채용이 단순해지며 팀 내 지식 전파가 더 쉬워집니다. Deepkit은 아이소모픽 TypeScript의 강점을 온전히 활용하여, 전통적인 듀얼 스택 접근을 크게 능가하는 통합된 크로스 플랫폼 개발 경험을 제공합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm.md b/website/src/translations/ko/documentation/orm.md new file mode 100644 index 000000000..831cb51d6 --- /dev/null +++ b/website/src/translations/ko/documentation/orm.md @@ -0,0 +1,15 @@ +# Deepkit ORM + +Deepkit ORM은 고성능 TypeScript ORM (Object-Relational Mapper)입니다. 이는 데이터베이스와 상호작용하기 위한 간단하고 직관적인 API를 제공하여, 저수준 데이터베이스 작업에 신경 쓰기보다 애플리케이션 구축에 집중할 수 있게 해줍니다. 이 ORM은 Deepkit의 Runtime Type System 위에 구축되어 있으며, 데이터베이스 작업을 위한 type-safe 환경을 제공합니다. + +## 왜 ORM인가? + +Deepkit의 Object-Relational Mapping(ORM)은 개발자에게 여러 이점을 제공합니다. + +1. 단순화된 데이터베이스 작업: ORM을 사용하면 개발자는 SQL 쿼리의 수동 생성과 실행을 추상화할 수 있습니다. 대신 더 직관적인 객체 지향적 접근 방식으로 데이터베이스와 상호작용할 수 있습니다. 이는 조회, 삽입, 업데이트, 삭제와 같은 일반적인 데이터베이스 작업을 단순화합니다. + +2. Cross-Database 호환성: ORM은 서로 다른 데이터베이스 시스템과 상호작용하기 위한 일관된 API를 제공하여, 개발자가 데이터베이스에 종속되지 않은 코드를 작성할 수 있도록 합니다. 즉, MySQL, PostgreSQL, SQLite와 같은 다양한 데이터베이스 엔진 간을 코드베이스에 큰 변경 없이 쉽게 전환할 수 있습니다. + +3. 타입 안전성과 컴파일 타임 검사: 런타임 타입 정보를 활용하여 Deepkit의 ORM은 데이터베이스 작업을 위한 type-safe 환경을 제공합니다. ORM을 사용하면 데이터베이스 스키마를 TypeScript Class 또는 Interface로 정의할 수 있어, 런타임이 아니라 컴파일 타임에 잠재적인 오류를 잡을 수 있습니다. 또한 ORM은 자동 타입 변환과 검증을 처리하여, 데이터가 항상 일관되며 데이터베이스에 올바르게 영속화되도록 보장합니다. + +종합적으로, Deepkit에서 ORM을 사용하면 데이터베이스 작업이 단순화되고 cross-database 호환성이 향상되며, 타입 안전성과 컴파일 타임 검사가 제공되어 견고하고 유지보수 가능한 애플리케이션을 구축하는 데 필수적인 구성 요소가 됩니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/composite-primary-key.md b/website/src/translations/ko/documentation/orm/composite-primary-key.md new file mode 100644 index 000000000..26e033748 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/composite-primary-key.md @@ -0,0 +1,84 @@ +# 복합 Primary Key + +Composite Primary Key란, 하나의 엔티티가 여러 개의 Primary Key를 가지며 이들이 자동으로 결합되어 "composite primary key"를 이루는 것을 의미합니다. 이러한 방식의 데이터베이스 모델링에는 장단점이 있습니다. 우리는 Composite Primary Key가 그 장점에 비해 실무적인 단점이 매우 크다고 판단하며, 따라서 bad practice로 간주되어 피해야 한다고 봅니다. Deepkit ORM은 Composite Primary Key를 지원하지 않습니다. 이 장에서는 그 이유를 설명하고 (더 나은) 대안을 제시합니다. + +## 단점 + +조인은 단순하지 않습니다. RDBMS에서 매우 최적화되어 있더라도, 애플리케이션에서는 쉽게 통제 불가능한 상수 복잡도를 만들어 성능 문제로 이어질 수 있습니다. 성능은 쿼리 실행 시간뿐 아니라 개발 시간 측면에서도 해당됩니다. + +## 조인 + +각 개별 조인은 관여하는 필드가 많아질수록 더 복잡해집니다. 많은 데이터베이스가 여러 필드로 조인하더라도 본질적으로 느려지지 않도록 최적화를 구현했지만, 개발자는 이러한 조인을 항상 세심하게 검토해야 합니다. 예를 들어 키를 하나라도 빠뜨리면 미묘한 오류로 이어질 수 있습니다(모든 키를 명시하지 않아도 조인이 동작하기 때문). 따라서 개발자는 전체 Composite Primary Key 구조를 알고 있어야 합니다. + +## 인덱스 + +여러 필드로 구성된 인덱스(즉, Composite Primary Key)는 쿼리에서의 필드 순서 문제를 겪습니다. 데이터베이스 시스템이 특정 쿼리를 최적화할 수는 있지만, 구조가 복잡해질수록 모든 정의된 인덱스를 올바르게 사용하는 효율적인 연산을 작성하기가 어려워집니다. 여러 필드가 있는 인덱스(예: Composite Primary Key)의 경우, 데이터베이스가 실제로 인덱스를 사용하도록 필드를 올바른 순서로 정의하는 것이 일반적으로 필요합니다. 순서가 올바르게 지정되지 않으면(예: WHERE 절에서) 데이터베이스가 인덱스를 전혀 사용하지 않고 전체 테이블 스캔을 수행할 수 있습니다. 어떤 데이터베이스 쿼리가 어떤 방식으로 최적화되는지 아는 것은 고급 지식이며, 신규 개발자에게는 보통 없는 지식입니다. 그러나 Composite Primary Key를 사용하기 시작하면 데이터베이스를 최대한 활용하고 리소스를 낭비하지 않기 위해 이러한 지식이 필요해집니다. + +## 마이그레이션 + +특정 엔티티를 고유하게 식별하기 위해 추가 필드가 필요하다고 결정하는 순간(즉, Composite Primary Key가 됨), 해당 엔티티와 관계를 가진 데이터베이스의 모든 엔티티를 조정해야 합니다. + +예를 들어, `user` 엔티티가 Composite Primary Key를 가지고 있고, 여러 테이블(예: 피벗 테이블 `audit_log`, `groups`, `posts`)에서 이 `user`에 대한 외래 키를 사용한다고 가정해 봅시다. `user`의 Primary Key를 변경하는 순간, 이러한 모든 테이블도 마이그레이션에서 함께 조정되어야 합니다. + +이는 마이그레이션 파일을 훨씬 더 복잡하게 만들 뿐만 아니라, 마이그레이션 실행 시 큰 다운타임을 초래할 수 있습니다. 스키마 변경은 보통 전체 데이터베이스 락 또는 적어도 테이블 락을 필요로 하기 때문입니다. 인덱스 변경과 같은 큰 변경으로 영향을 받는 테이블이 많을수록 마이그레이션 시간은 길어집니다. 그리고 테이블이 클수록 마이그레이션 시간은 더 오래 걸립니다. +`audit_log` 테이블을 생각해 보세요. 이러한 테이블은 보통 매우 많은 레코드(수백만 건 등)를 가지고 있으며, 오직 Composite Primary Key를 사용하기로 결정했고 `user`의 Primary Key에 필드를 하나 더 추가했다는 이유만으로 스키마 변경 중에 이들을 모두 건드려야 합니다. 이러한 모든 테이블의 크기에 따라, 이는 마이그레이션 변경을 불필요하게 더 비싸게 만들거나, 경우에 따라 `User`의 Primary Key 변경이 더 이상 재정적으로 정당화될 수 없을 만큼 비싸게 만들 수도 있습니다. 이는 보통(예: user 테이블에 unique 인덱스를 추가하는 등의) 우회책으로 이어지며, 기술 부채를 낳고 머지않아 레거시 목록에 오르게 됩니다. + +대규모 프로젝트에서는 이는 큰 다운타임(수 분에서 수 시간)에 이를 수 있으며, 심지어 본질적으로 테이블을 복사하고, 레코드를 고스트 테이블에 삽입한 뒤, 마이그레이션 후 테이블을 앞뒤로 이동시키는 전혀 새로운 마이그레이션 추상화 시스템의 도입으로 이어지기도 합니다. 이러한 추가 복잡성은 Composite Primary Key를 가진 다른 엔티티와 관계를 가진 모든 엔티티에 강제로 부과되며, 데이터베이스 구조가 커질수록 더 커집니다. 이 문제는(Composite Primary Key를 완전히 제거하는 것 외에는) 해결 방법이 없어 점점 악화됩니다. + +## 찾기 용이성 + +데이터베이스 관리자나 Data Engineer/Scientist라면 보통 데이터베이스에서 직접 작업하고, 필요할 때 데이터를 탐색합니다. Composite Primary Key를 사용하면, SQL을 직접 작성하는 모든 사용자는 관련된 모든 테이블의 올바른 Primary Key(그리고 올바른 인덱스 최적화를 위한 컬럼 순서) 를 알아야 합니다. 이러한 오버헤드는 데이터 탐색, 리포트 생성 등을 복잡하게 만들 뿐 아니라, Composite Primary Key가 갑자기 변경되면 오래된 SQL에서 오류를 유발할 수 있습니다. 오래된 SQL은 아마도 여전히 유효하고 잘 실행되겠지만, 조인에서 Composite Primary Key의 새 필드가 누락되어 갑자기 잘못된 결과를 반환합니다. 여기서는 Primary Key를 하나만 두는 것이 훨씬 쉽습니다. 이렇게 하면 데이터를 찾기 쉬워지고, 예를 들어 user 객체를 고유하게 식별하는 방식을 변경하기로 하더라도 오래된 SQL 쿼리가 여전히 올바르게 동작하도록 보장할 수 있습니다. + +## 리팩토링 + +하나의 엔티티에서 Composite Primary Key를 사용하게 되면, 키를 리팩토링할 때 상당한 추가 리팩토링이 필요할 수 있습니다. Composite Primary Key를 가진 엔티티는 일반적으로 단일 고유 필드가 없으므로, 모든 필터와 링크에는 Composite Key의 모든 값이 포함되어야 합니다. 이는 보통 코드가 Composite Primary Key를 알고 있다는 가정에 의존하게 된다는 뜻이며, 따라서 모든 필드를 가져와야 합니다(예: user:key1:key2 같은 URL). 일단 이 키가 변경되면, URL, 커스텀 SQL 쿼리 등 이 지식이 명시적으로 사용되는 모든 곳을 다시 작성해야 합니다. + +ORM은 일반적으로 값을 수동으로 지정하지 않아도 조인을 자동으로 생성하지만, URL 구조나 커스텀 SQL 쿼리 같은 다른 모든 사용 사례에 대한 리팩토링을 자동으로 처리할 수 없으며, 특히 리포팅 시스템 및 모든 외부 시스템 등 ORM을 전혀 사용하지 않는 곳에서는 더욱 그렇습니다. + +## ORM 복잡성 + +Composite Primary Key를 지원하면 Deepkit ORM과 같은 강력한 ORM의 코드 복잡성이 엄청나게 증가합니다. 코드와 유지보수가 더 복잡해져 비용이 증가할 뿐 아니라, 사용자로부터 발생하는 엣지 케이스가 늘어나 이를 수정·유지해야 합니다. 쿼리 레이어, 변경 감지, 마이그레이션 시스템, 내부 관계 추적 등 전반의 복잡성이 크게 증가합니다. Composite Primary Key를 지원하는 ORM을 구축하고 지원하는 데 수반되는 전체 비용은 전반적으로 너무 높아 정당화될 수 없기에, Deepkit은 이를 지원하지 않습니다. + +## 장점 + +이와 별개로, Composite Primary Key에도 장점은 있습니다. 다만 매우 피상적입니다. 각 테이블에 가능한 적은 인덱스를 사용하면, 유지해야 할 인덱스가 적어 쓰기(삽입/수정)가 더 효율적입니다. 또한 모델의 구조가 약간 더 깔끔해지기도 합니다(보통 컬럼이 하나 적어지므로). 그러나 요즘에는 순차적으로 정렬된 자동 증가 Primary Key와 비증가형 Primary Key 간의 차이는 사실상 무시할 수 있습니다. 디스크 공간은 저렴하고, 해당 작업은 보통 "append-only" 작업으로 매우 빠르기 때문입니다. + +물론 소수의 엣지 케이스(그리고 매우 특정한 일부 데이터베이스 시스템)에서는 처음에는 Composite Primary Key로 작업하는 것이 더 나을 수 있습니다. 하지만 그러한 시스템에서도(모든 비용을 고려하면) 이를 사용하지 않고 다른 전략으로 전환하는 편이 전반적으로 더 타당할 수 있습니다. + +## 대안 + +Composite Primary Key의 대안은 단일 자동 증가 숫자 Primary Key(보통 "id")를 사용하고, Composite Primary Key는 여러 필드로 구성된 unique 인덱스로 옮기는 것입니다. 사용되는 Primary Key(예상 행 수에 따라 다름)에 따라 "id"는 레코드당 4바이트 또는 8바이트를 사용합니다. + +이 전략을 사용하면, 더 이상 위에서 설명한 문제들을 강제로 고민하고 해결책을 찾아야 하지 않으므로, 끊임없이 성장하는 프로젝트의 비용을 크게 줄일 수 있습니다. + +이 전략은 구체적으로 각 엔티티가 보통 가장 앞에 "id" 필드를 하나 가지고, 기본적으로 이 필드를 고유 행 식별 및 조인에 사용한다는 뜻입니다. + +```typescript +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor(public username: string) {} +} +``` + +Composite Primary Key의 대안으로, 대신 여러 필드로 구성된 unique 인덱스를 사용합니다. + +```typescript +@entity.index(['tenancyId', 'username'], {unique: true}) +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public tenancyId: number, + public username: string, + ) {} +} +``` + +Deepkit ORM은 MongoDB를 포함해 자동 증가 Primary Key를 자동으로 지원합니다. 이는 데이터베이스에서 레코드를 식별하는 데 선호되는 방법입니다. 다만, MongoDB의 경우 간단한 Primary Key로 ObjectId(`_id: MongoId & PrimaryKey = ''`)를 사용할 수 있습니다. 숫자형 자동 증가 Primary Key의 대안으로 UUID도 동일하게 사용할 수 있습니다(단, 인덱싱 비용이 더 비싸므로 약간 다른 성능 특성이 있습니다). + +## 요약 + +Composite Primary Key는 본질적으로 한 번 도입되면 향후 모든 변경과 실무적 사용의 비용이 훨씬 높아진다는 것을 의미합니다. 처음에는(컬럼이 하나 적으므로) 깔끔한 아키텍처처럼 보일 수 있지만, 프로젝트가 실제로 개발되기 시작하면 실무적인 비용이 크게 증가하며, 프로젝트가 커질수록 그 비용은 계속 상승합니다. + +장점과 단점의 비대칭성을 고려하면, 대부분의 경우 Composite Primary Key는 정당화될 수 없습니다. 비용이 이점보다 훨씬 큽니다. 사용자뿐 아니라 ORM 코드를 작성·유지하는 우리에게도 마찬가지입니다. 이러한 이유로 Deepkit ORM은 Composite Primary Key를 지원하지 않습니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/entity.md b/website/src/translations/ko/documentation/orm/entity.md new file mode 100644 index 000000000..c7dbf44bb --- /dev/null +++ b/website/src/translations/ko/documentation/orm/entity.md @@ -0,0 +1,226 @@ +# 엔터티 + +엔터티는 클래스 또는 객체 리터럴(인터페이스)이며 항상 기본 키를 가집니다. +엔터티는 `@deepkit/type`의 타입 애너테이션을 사용해 필요한 모든 정보를 데코레이션합니다. 예를 들어, 기본 키와 다양한 필드 및 그 검증 제약을 정의합니다. 이 필드들은 일반적으로 테이블 또는 컬렉션인 데이터베이스 구조를 반영합니다. + +`Mapped<'name'>`와 같은 특수 타입 애너테이션을 통해 필드 이름을 데이터베이스의 다른 이름으로 매핑할 수도 있습니다. + +## 클래스 + +```typescript +import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: string & Unique & MinLength<2> & MaxLength<16>, + public email: string & Unique, + ) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +await database.migrate(); + +await database.persist(new User('Peter')); + +const allUsers = await database.query(User).find(); +console.log('all users', allUsers); +``` + +## 인터페이스 + +```typescript +import { PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + username: string & Unique & MinLength<2> & MaxLength<16>; +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:')); +database.register({name: 'user'}); + +await database.migrate(); + +const user: User = {id: 0, created: new Date, username: 'Peter'}; +await database.persist(user); + +const allUsers = await database.query().find(); +console.log('all users', allUsers); +``` + +## 원시 타입 + +String, Number (bigint), Boolean 같은 원시 데이터 타입은 일반적인 데이터베이스 타입으로 매핑됩니다. TypeScript 타입만 사용됩니다. + +```typescript + +interface User { + logins: number; + username: string; + pro: boolean; +} +``` + +## 기본 키 + +각 엔터티에는 정확히 하나의 기본 키가 필요합니다. 다중 기본 키는 지원되지 않습니다. + +기본 키의 기본 타입은 임의일 수 있습니다. 보통 number 또는 UUID가 사용됩니다. +MongoDB에서는 종종 MongoId 또는 ObjectID가 사용됩니다. + +number의 경우 `AutoIncrement`를 사용할 수 있습니다. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## 자동 증가 + +삽입 시 자동으로 증가되어야 하는 필드는 `AutoIncrement` 데코레이터로 애너테이션합니다. 모든 어댑터가 auto-increment 값을 지원합니다. MongoDB 어댑터는 카운터를 추적하기 위해 추가 컬렉션을 사용합니다. + +auto-increment 필드는 자동 카운터이며 기본 키에만 적용할 수 있습니다. 데이터베이스는 ID가 한 번만 사용되도록 자동으로 보장합니다. + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## UUID + +UUID(v4) 타입이어야 하는 필드는 UUID 데코레이터로 애너테이션합니다. 런타임 타입은 `string`이며, 데이터베이스에서는 대부분 바이너리입니다. 새로운 UUID v4를 생성하려면 `uuid()` 함수를 사용하세요. + +```typescript +import { uuid, UUID, PrimaryKey } from '@deepkit/type'; + +class User { + id: UUID & PrimaryKey = uuid(); +} +``` + +## MongoDB ObjectID + +MongoDB에서 ObjectID 타입이어야 하는 필드는 `MongoId` 데코레이터로 애너테이션합니다. 런타임 타입은 `string`이고, 데이터베이스에서는 `ObjectId`(바이너리)입니다. + +MongoID 필드는 삽입 시 자동으로 새로운 값을 받습니다. 필드 이름으로 반드시 `_id`를 사용할 필요는 없습니다. 아무 이름이나 사용할 수 있습니다. + +```typescript +import { PrimaryKey, MongoId } from '@deepkit/type'; + +class User { + id: MongoId & PrimaryKey = ''; +} +``` + +## 옵셔널 / Nullable + +옵셔널 필드는 TypeScript 타입으로 `title?: string` 또는 `title: string | null`로 선언합니다. 보통 `undefined`와 함께 동작하는 옵셔널 `?` 문법을 한 가지만 사용하는 것이 좋습니다. +두 가지 방식 모두 모든 SQL 어댑터에서 데이터베이스 타입이 `NULLABLE`이 됩니다. 따라서 이들 사이의 유일한 차이는 런타임에서 서로 다른 값을 표현한다는 점입니다. + +다음 예시에서 modified 필드는 옵셔널이므로 런타임에서는 undefined가 될 수 있지만, 데이터베이스에서는 항상 NULL로 표현됩니다. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified?: Date; +} +``` + +이 예시는 nullable 타입이 어떻게 동작하는지 보여줍니다. 데이터베이스와 JavaScript 런타임 모두에서 NULL이 사용됩니다. 이는 `modified?: Date`보다 장황하며 일반적으로는 잘 사용되지 않습니다. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified: Date | null = null; +} +``` + +## 데이터베이스 타입 매핑 + +|=== +|런타임 타입|SQLite|MySQL|Postgres|Mongo + +|string|text|longtext|text|string +|number|float|double|double precision|int/number +|boolean|integer(1)|boolean|boolean|boolean +|date|text|datetime|timestamp|datetime +|array|text|json|jsonb|array +|map|text|json|jsonb|object +|map|text|json|jsonb|object +|union|text|json|jsonb|T +|uuid|blob|binary(16)|uuid|binary +|ArrayBuffer/Uint8Array/...|blob|longblob|bytea|binary +|=== + +`DatabaseField`를 사용하면 필드를 임의의 데이터베이스 타입으로 매핑할 수 있습니다. 타입은 마이그레이션 시스템에 변경 없이 전달되는 유효한 SQL 문이어야 합니다. + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + title: string & DatabaseField<{type: 'VARCHAR(244)'}>; +} +``` + +특정 데이터베이스에 대해 필드를 매핑하려면 `SQLite`, `MySQL`, 또는 `Postgres`를 사용할 수 있습니다. + +### SQLite + +```typescript +import { SQLite } from '@deepkit/type'; + +interface User { + title: string & SQLite<{type: 'text'}>; +} +``` + +### MySQL + +```typescript +import { MySQL } from '@deepkit/type'; + +interface User { + title: string & MySQL<{type: 'text'}>; +} +``` + +### Postgres + +```typescript +import { Postgres } from '@deepkit/type'; + +interface User { + title: string & Postgres<{type: 'text'}>; +} +``` + +## 임베디드 타입 + +## 기본값 + +## 기본 표현식 + +## 복합 타입 + +## 제외 + +## 데이터베이스별 컬럼 타입 \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/events.md b/website/src/translations/ko/documentation/orm/events.md new file mode 100644 index 000000000..6503c0877 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/events.md @@ -0,0 +1,67 @@ +# 이벤트 + +이벤트는 Deepkit ORM에 hook 하여 강력한 플러그인을 작성할 수 있는 방법입니다. 이벤트에는 두 가지 +카테고리가 있습니다: Query 이벤트와 Unit-of-Work 이벤트. 플러그인 작성자는 일반적으로 둘 다를 사용하여 +데이터를 조작하는 두 가지 방식을 모두 지원합니다. + +이벤트는 `Database.listen`을 통해 event token에 등록됩니다. 수명이 짧은 이벤트 리스너는 세션에도 +등록할 수 있습니다. + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); +database.listen(Query.onFetch, async (event) => { +}); + +const session = database.createSession(); + +//이 특정 세션에서만 실행됩니다 +session.eventDispatcher.listen(Query.onFetch, async (event) => { +}); +``` + +## Query 이벤트 + +Query 이벤트는 `Database.query()` 또는 `Session.query()`를 통해 query가 실행될 때 트리거됩니다. + +각 이벤트에는 entity 타입, query 자체, 데이터베이스 세션 등 고유한 추가 속성이 있습니다. +`Event.query`에 새로운 query를 설정하여 query를 override할 수 있습니다. + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); + +const unsubscribe = database.listen(Query.onFetch, async event => { + //사용자의 쿼리를 덮어써서 다른 것이 실행되도록 합니다. + event.query = event.query.filterField('fieldName', 123); +}); + +//hook을 제거하려면 unsubscribe를 호출하세요 +unsubscribe(); +``` + +"Query"에는 여러 Event token이 있습니다: + +| Event-Token | 설명 | +|--------------------|--------------------------------------------------------------| +| Query.onFetch | find()/findOne()/등을 통해 객체가 조회되었을 때 | +| Query.onDeletePre | deleteMany/deleteOne()를 통해 객체가 삭제되기 전 | +| Query.onDeletePost | deleteMany/deleteOne()를 통해 객체가 삭제된 후 | +| Query.onPatchPre | patchMany/patchOne()를 통해 객체가 패치/업데이트되기 전 | +| Query.onPatchPost | patchMany/patchOne()를 통해 객체가 패치/업데이트된 후 | + +## Unit Of Work 이벤트 + +Unit-of-Work 이벤트는 새로운 세션이 변경 사항을 제출할 때 트리거됩니다. + +| Event-Token | 설명 | +|------------------------------|----------------------------------------------------------------------------------------------| +| DatabaseSession.onUpdatePre | `DatabaseSession` 객체가 데이터베이스 레코드에 대한 업데이트 작업을 시작하기 직전에 트리거됩니다. | +| DatabaseSession.onUpdatePost | `DatabaseSession` 객체가 업데이트 작업을 성공적으로 완료한 직후 트리거됩니다. | +| DatabaseSession.onInsertPre | `DatabaseSession` 객체가 새 레코드를 데이터베이스에 삽입하기 시작하기 직전에 트리거됩니다. | +| DatabaseSession.onInsertPost | `DatabaseSession` 객체가 새 레코드를 성공적으로 삽입한 직후 트리거됩니다. | +| DatabaseSession.onDeletePre | `DatabaseSession` 객체가 데이터베이스에서 레코드를 삭제하기 시작하기 직전에 트리거됩니다. | +| DatabaseSession.onDeletePost | `DatabaseSession` 객체가 삭제 작업을 완료한 직후 트리거됩니다. | +| DatabaseSession.onCommitPre | `DatabaseSession` 객체가 세션 동안 이루어진 변경 사항을 데이터베이스에 커밋하기 직전에 트리거됩니다. | \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/getting-started.md b/website/src/translations/ko/documentation/orm/getting-started.md new file mode 100644 index 000000000..5aaeed8d1 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/getting-started.md @@ -0,0 +1,191 @@ +# 시작하기 + +Deepkit은 데이터베이스에 현대적인 방식으로 접근할 수 있게 해주는 Database ORM을 제공합니다. +엔티티는 TypeScript 타입을 사용하여 간단히 정의됩니다: + +```typescript +import { entity, PrimaryKey, AutoIncrement, + Unique, MinLength, MaxLength } from '@deepkit/type'; + +type Username = string & Unique & MinLength<2> & MaxLength<16>; + +// 클래스 엔티티 +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: Username, + public email: string & Unique, + ) {} +} + +// 또는 interface로 +interface User { + id: number & PrimaryKey & AutoIncrement; + created: Date; + firstName?: string; + lastName?: string; + username: Username; + email: string & Unique; +} +``` + +Deepkit의 모든 TypeScript 타입과 검증 Decorator를 사용해 엔티티를 완전히 정의할 수 있습니다. +엔티티 타입 시스템은 이러한 타입이나 클래스가 HTTP routes, RPC actions, 또는 frontend와 같은 다른 영역에서도 사용할 수 있도록 설계되었습니다. 이는 예를 들어 애플리케이션 전체에 동일한 사용자를 여러 번 분산 정의하는 일을 방지합니다. + +## 설치 + +Deepkit ORM은 Runtime Types에 기반하므로, `@deepkit/type`이 올바르게 설치되어 있어야 합니다. +[Runtime Type 설치](../runtime-types/getting-started.md)를 참조하세요. + +이 작업이 완료되면, `@deepkit/orm` 자체와 데이터베이스 어댑터를 설치할 수 있습니다. + +클래스를 엔티티로 사용하려면 tsconfig.json에서 `experimentalDecorators`를 활성화해야 합니다: + +```json +{ + "compilerOptions": { + "experimentalDecorators": true + } +} +``` + +라이브러리가 설치되면, 데이터베이스 어댑터를 설치하고 해당 API를 바로 사용할 수 있습니다. + +### SQLite + +```sh +npm install @deepkit/orm @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; + +const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +``` + +### MySQL + +```sh +npm install @deepkit/orm @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; + +const database = new Database(new MySQLDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### Postgres + +```sh +npm install @deepkit/orm @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/postgres'; + +const database = new Database(new PostgresDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### MongoDB + +```sh +npm install @deepkit/orm @deepkit/bson @deepkit/mongo +``` + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(new MongoDatabaseAdapter('mongodb://localhost/mydatabase'), [User]); +``` + +## 사용법 + +주로 `Database` 객체를 사용합니다. 한 번 인스턴스화되면 애플리케이션 전반에서 데이터를 조회하거나 조작하는 데 사용할 수 있습니다. 데이터베이스 연결은 지연 초기화(lazy)됩니다. + +`Database` 객체에는 데이터베이스 어댑터 라이브러리에서 제공되는 어댑터를 전달합니다. + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + @entity.name('user') + class User { + public id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); + await database.migrate(); // 테이블 생성 + + await database.persist(new User('Peter')); + + const allUsers = await database.query(User).find(); + console.log('all users', allUsers); +} + +main(); +``` + +### 데이터베이스 + +### 연결 + +#### 읽기 복제본 + +## 리포지토리 + +## 인덱스 + +## 대소문자 구분 + +## 문자 집합 + +## 정렬 규칙 + +## 배치 처리 + +## 캐싱 + +## 멀티테넌시 + +## 네이밍 전략 + +## 잠금 + +### 낙관적 잠금 + +### 비관적 잠금 + +## 커스텀 타입 + +## 로깅 + +## 마이그레이션 + +## 시딩 + +## 원시 데이터베이스 액세스 + +### SQL + +### MongoDB + +## 플러그인 \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/inheritance.md b/website/src/translations/ko/documentation/orm/inheritance.md new file mode 100644 index 000000000..121f3ceaf --- /dev/null +++ b/website/src/translations/ko/documentation/orm/inheritance.md @@ -0,0 +1,104 @@ +# 상속 + +Deepkit ORM에서 상속을 구현하는 방법은 여러 가지가 있습니다. + +## 클래스 상속 + +한 가지 방법은 `extends`를 사용하는 간단한 클래스들을 통한 클래스 상속을 이용하는 것입니다. + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +class BaseModel { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + updated: Date = new Date; +} + +class User extends BaseModel { + name: string = ''; +} + +class Customer extends BaseModel { + name: string = ''; + address: string = ''; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [User, Customer] +); +``` + +`BaseModel`은 엔티티로 사용되지 않으므로 데이터베이스에 등록되지 않습니다. `User`와 `Customer`만 엔티티로 등록되며, `BaseModel`의 모든 프로퍼티가 포함된 테이블에 매핑됩니다. + +SQL 테이블은 다음과 같습니다: + +```sql +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL +); + +CREATE TABLE customer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL, + address TEXT NOT NULL +); +``` + +## 단일 테이블 상속 + +단일 테이블 상속(Single Table Inheritance)은 여러 엔티티를 하나의 테이블에 저장하는 방식입니다. 각 모델마다 별도의 테이블을 두는 대신 단일 테이블을 사용하고, 각 레코드의 타입을 판별하기 위해 추가 컬럼(보통 type과 같은 이름)을 사용합니다. 동일한 프로퍼티를 공유하는 엔티티가 많을 때 유용합니다. + +```typescript +import { PrimaryKey, AutoIncrement, entity } from '@deepkit/type'; + +@entity.collection('persons') +abstract class Person { + id: number & PrimaryKey & AutoIncrement = 0; + firstName?: string; + lastName?: string; + abstract type: string; +} + +@entity.singleTableInheritance() +class Employee extends Person { + email?: string; + + type: 'employee' = 'employee'; +} + +@entity.singleTableInheritance() +class Freelancer extends Person { + @t budget: number = 10_000; + + type: 'freelancer' = 'freelancer'; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [Employee, Freelancer] +); +``` + +`Person` 클래스는 엔티티가 아니므로 데이터베이스에 등록되지 않습니다. `Employee`와 `Freelancer` 클래스는 엔티티이며 `persons`라는 이름의 단일 테이블에 매핑됩니다. `type` 컬럼은 각 레코드의 타입을 판별하는 데 사용됩니다. + +SQL 테이블은 다음과 같습니다: + +```sql +CREATE TABLE persons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + firstName TEXT, + lastName TEXT, + type TEXT NOT NULL, + email TEXT, + budget INTEGER +); +``` + +보시다시피 budget은 선택적(optional)로 처리됩니다(비록 `Freelance` 클래스에서는 필수이지만). 이는 budget 값이 없는 `Employee`를 동일한 테이블에 저장할 수 있도록 하기 위함입니다. 이는 단일 테이블 상속의 한계입니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/migrations.md b/website/src/translations/ko/documentation/orm/migrations.md new file mode 100644 index 000000000..6558e2daa --- /dev/null +++ b/website/src/translations/ko/documentation/orm/migrations.md @@ -0,0 +1,73 @@ +# 마이그레이션 + +마이그레이션은 데이터베이스 스키마 변경을 구조적이고 체계적으로 관리하는 방법입니다. 디렉터리에 TypeScript 파일로 저장되며, 명령줄 도구를 사용해 실행할 수 있습니다. + +Deepkit Framework를 사용할 때 Deepkit ORM 마이그레이션은 기본으로 활성화됩니다. + +## 명령어 + +- `migration:create` - 데이터베이스 diff를 기반으로 새로운 마이그레이션 파일을 생성합니다 +- `migration:pending` - 보류 중인 마이그레이션 파일을 보여줍니다 +- `migration:up` - 보류 중인 마이그레이션 파일을 실행합니다. +- `migration:down` - down 마이그레이션을 실행하여 이전 마이그레이션 파일을 되돌립니다 + +이 명령어는 애플리케이션에서 `FrameworkModule`을 import하면 사용할 수 있으며, 또는 `@deepkit/sql`의 `deepkit-sql` 명령줄 도구를 통해 사용할 수 있습니다. + +[FrameworkModule의 마이그레이션 통합](../framework/database.md#migration)은 여러분의 여러 Database(이를 provider로 정의해야 함)를 자동으로 읽어들이고, `deepkit-sql`을 사용하는 경우에는 Database를 export하는 TypeScript 파일을 지정해야 합니다. 후자는 Deepkit Framework 없이 Deepkit ORM을 단독(standalone)으로 사용할 때 유용합니다. + +## 마이그레이션 생성 + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + name = 'default'; + constructor() { + super(new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), [User]); + } +} +``` + +```sh +./node_modules/.bin/deepkit-sql migration:create --path database.ts --migrationDir src/migrations +``` + +새 마이그레이션 파일이 `src/migrations`에 생성됩니다. + +새로 생성된 마이그레이션 파일에는 TypeScript 앱에 정의된 엔티티와 설정된 데이터베이스 간의 차이를 기반으로 한 up 및 down Method가 포함됩니다. 이제 필요에 맞게 up Method를 수정할 수 있습니다. down Method는 up Method를 기반으로 자동 생성됩니다. +이 파일을 저장소에 커밋하면 다른 개발자도 이를 실행할 수 있습니다. + +## 보류 중인 마이그레이션 + +```sh +./node_modules/.bin/deepkit-sql migration:pending --path database.ts --migrationDir src/migrations +``` + +모든 보류 중인 마이그레이션을 표시합니다. 아직 실행되지 않은 새 마이그레이션 파일이 있다면 여기 목록에 나타납니다. + +## 마이그레이션 실행 + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations +``` + +다음 보류 중인 마이그레이션을 실행합니다. + +## 마이그레이션 되돌리기 + +```sh +./node_modules/.bin/deepkit-sql migration:down --path database.ts --migrationDir src/migrations +``` + +마지막으로 실행된 마이그레이션을 되돌립니다. + +## Fake 마이그레이션 + +마이그레이션(up 또는 down)을 실행하려고 했지만 실패했다고 가정해 봅시다. 문제를 수동으로 고쳤지만, 이미 실행된 것으로 표시되어 마이그레이션을 다시 실행할 수 없습니다. 이때 `--fake` 옵션을 사용하여 실제로 실행하지 않고도 데이터베이스에 실행된 것으로 표시(faking)할 수 있습니다. 이렇게 하면 다음 보류 중인 마이그레이션을 실행할 수 있습니다. + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/orm-browser.md b/website/src/translations/ko/documentation/orm/orm-browser.md new file mode 100644 index 000000000..ae4876f3e --- /dev/null +++ b/website/src/translations/ko/documentation/orm/orm-browser.md @@ -0,0 +1,60 @@ +# ORM 브라우저 + +Deepkit ORM Browser는 데이터베이스 스키마와 데이터를 탐색하기 위한 웹 기반 도구입니다. Deepkit Framework 위에 구축되었으며, Deepkit ORM에서 지원하는 모든 데이터베이스와 함께 사용할 수 있습니다. + +![ORM 브라우저](/assets/screenshots-orm-browser/content-editing.png) + +## 설치 + +Deepkit ORM Browser는 Deepkit Framework의 일부이며 debug 모드가 활성화되면 함께 활성화됩니다. + +```typescript +import { App } from '@deepkit/app'; +import { Database } from '@deepkit/orm'; + +class MyController { + @http.GET('/') + index() { + return 'Hello World'; + } +} + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +new App({ + controllers: [MyController], + providers: [MainDatabase], + imports: [new FrameworkModule({debug: true})], +}).run(); +``` + +또는, Deepkit ORM Browser를 독립 패키지로 설치할 수 있습니다. + +```bash +npm install @deepkit/orm-browser +``` + +```typescript +// database.ts +import { Database } from '@deepkit/orm'; + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +export const database = new MainDatabase(); +``` + +다음으로, Deepkit ORM Browser 서버를 시작할 수 있습니다. + +```sh +./node_modules/.bin/deepkit-orm-browser database.ts +``` + +이제 http://localhost:9090에서 Deepkit ORM Browser를 사용할 수 있습니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/plugin-soft-delete.md b/website/src/translations/ko/documentation/orm/plugin-soft-delete.md new file mode 100644 index 000000000..8b08d0b37 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/plugin-soft-delete.md @@ -0,0 +1,112 @@ +# Soft-Delete + +Soft-Delete plugin은 데이터베이스 레코드를 실제로 삭제하지 않고 숨긴 상태로 유지할 수 있게 합니다. 레코드가 삭제되면 실제로 삭제되는 것이 아니라 삭제된 것으로만 표시됩니다. 모든 query는 이 deleted 속성을 자동으로 필터링하므로, 사용자에게는 실제로 삭제된 것처럼 보입니다. + +plugin을 사용하려면 SoftDelete class를 인스턴스화하고 각 entity에 대해 활성화해야 합니다. + +```typescript +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { SoftDelete } from '@deepkit/orm'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + // 이 필드는 레코드가 soft deleted 상태인지의 표시자로 사용됩니다. + // 값이 설정되면 해당 레코드는 soft deleted 상태입니다. + deletedAt?: Date; + + // 이 필드는 선택 사항이며, 누가/무엇이 레코드를 삭제했는지 추적하는 데 사용할 수 있습니다. + deletedBy?: string; + + constructor( + public name: string + ) { + } +} + +const softDelete = new SoftDelete(database); +softDelete.enable(User); + +// 또는 다시 비활성화 +softDelete.disable(User); +``` + +## 삭제 + +레코드를 soft-delete하려면 일반적인 방법을 사용하세요: query에서 `deleteOne` 또는 `deleteMany`를 사용하거나 session을 사용해 삭제하세요. soft-delete plugin이 나머지는 백그라운드에서 자동으로 처리합니다. + +## 복원 + +삭제된 레코드는 `SoftDeleteQuery`를 통해 'lifted' query로 복원할 수 있습니다. `restoreOne`과 `restoreMany`가 있습니다. + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreOne(); +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreMany(); +``` + +session 또한 요소 복원을 지원합니다. + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).restore(user1); +await session.commit(); +``` + +## 하드 삭제 + +레코드를 하드 삭제하려면 SoftDeleteQuery를 통해 lifted query를 사용하세요. 이는 본질적으로 soft-delete plugin이 사용되지 않는 일반 동작을 복원합니다. + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// 데이터베이스에서 레코드를 실제로 삭제 +await database.query(User).lift(SoftDeleteQuery).hardDeleteOne(); +await database.query(User).lift(SoftDeleteQuery).hardDeleteMany(); + +// 위와 동일합니다 +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteOne(); +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteMany(); +``` + +## 삭제된 항목 조회. + +`SoftDeleteQuery`를 통해 "lifted" query를 사용하면 삭제된 레코드도 포함할 수 있습니다. + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// soft deleted와 삭제되지 않은 항목을 포함하여 모두 조회 +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().find(); + +// soft deleted만 조회 +await database.query(s).lift(SoftDeleteQuery).isSoftDeleted().count() +``` + +## Deleted by + +`deletedBy`는 query와 session을 통해 설정할 수 있습니다. + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).setDeletedBy('Peter'); +session.remove(user1); + +await session.commit(); +import { SoftDeleteQuery } from '@deepkit/orm'; + +database.query(User).lift(SoftDeleteQuery) +.deletedBy('Peter') +.deleteMany(); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/query.md b/website/src/translations/ko/documentation/orm/query.md new file mode 100644 index 000000000..705244b63 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/query.md @@ -0,0 +1,356 @@ +# 쿼리 + +쿼리는 데이터베이스에서 데이터를 조회하거나 수정하는 방법을 기술하는 객체입니다. 쿼리를 기술하는 여러 Method와 이를 실행하는 종료 Method들을 갖습니다. 데이터베이스 adapter는 데이터베이스별 기능을 지원하기 위해 다양한 방식으로 Query API를 확장할 수 있습니다. + +`Database.query(T)` 또는 `Session.query(T)`를 사용해 쿼리를 생성할 수 있습니다. 성능 향상을 위해 Session 사용을 권장합니다. + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + birthdate?: Date; + visits: number = 0; + + constructor(public username: string) { + } +} + +const database = new Database(...); + +//[ { username: 'User1' }, { username: 'User2' }, { username: 'User2' } ] +const users = await database.query(User).select('username').find(); +``` + +## 필터 + +필터를 적용하여 결과 집합을 제한할 수 있습니다. + +```typescript +//간단한 필터 +const users = await database.query(User).filter({name: 'User1'}).find(); + +//여러 필터, 모두 AND +const users = await database.query(User).filter({name: 'User1', id: 2}).find(); + +//범위 필터: $gt, $lt, $gte, $lte (초과, 미만, 이상, 이하) +//WHERE created < NOW()와 동일 +const users = await database.query(User).filter({created: {$lt: new Date}}).find(); +//WHERE id > 500과 동일 +const users = await database.query(User).filter({id: {$gt: 500}}).find(); +//WHERE id >= 500과 동일 +const users = await database.query(User).filter({id: {$gte: 500}}).find(); + +//집합 필터: $in, $nin (in, not in) +//WHERE id IN (1, 2, 3)과 동일 +const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find(); + +//정규식 필터 +const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find(); + +//그룹화: $and, $nor, $or +//WHERE (username = 'User1') OR (username = 'User2')와 동일 +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2'}] +}).find(); + + +//중첩 그룹화 +//WHERE username = 'User1' OR (username = 'User2' and id > 0)와 동일 +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2', id: {$gt: 0}}] +}).find(); + + +//중첩 그룹화 +//WHERE username = 'User1' AND (created < NOW() OR id > 0)와 동일 +const users = await database.query(User).filter({ + $and: [{username: 'User1'}, {$or: [{created: {$lt: new Date}, id: {$gt: 0}}]}] +}).find(); +``` + +### 동등 (Equal) + +### 더 큼 / 더 작음 (Greater / Smaller) + +### RegExp + +### 그룹화 AND/OR + +### In + +## Select + +데이터베이스에서 받을 필드를 좁히기 위해 `select('field1')`을 사용할 수 있습니다. + +```typescript +const user = await database.query(User).select('username').findOne(); +const user = await database.query(User).select('id', 'username').findOne(); +``` + +`select`를 사용해 필드를 좁히는 즉시 결과는 더 이상 엔티티의 인스턴스가 아니라 단순 객체 리터럴이라는 점에 유의하세요. + +``` +const user = await database.query(User).select('username').findOne(); +user instanceof User; //false +``` + +## 정렬 (Order) + +`orderBy(field, order)`로 항목의 정렬 순서를 변경할 수 있습니다. +`orderBy`는 여러 번 실행하여 정렬을 점점 더 세밀하게 할 수 있습니다. + +```typescript +const users = await session.query(User).orderBy('created', 'desc').find(); +const users = await session.query(User).orderBy('created', 'asc').find(); +``` + +## 페이지네이션 (Pagination) + +`itemsPerPage()`와 `page()` 메서드를 사용해 결과를 페이지네이션할 수 있습니다. 페이지는 1부터 시작합니다. + +```typescript +const users = await session.query(User).itemsPerPage(50).page(1).find(); +``` + +대체 Method인 `limit`과 `skip`을 사용해 수동으로 페이지네이션할 수도 있습니다. + +```typescript +const users = await session.query(User).limit(5).skip(10).find(); +``` + +[#database-join] +## 조인 (Join) + +기본적으로 엔티티의 Reference는 쿼리에 포함되지도 않고 로드되지도 않습니다. Reference를 로드하지 않고 쿼리에 조인을 포함하려면 `join()`(LEFT JOIN) 또는 `innerJoin()`을 사용하세요. 쿼리에 조인을 포함하고 Reference를 로드하려면 `joinWith()` 또는 `innerJoinWith()`를 사용하세요. + +다음의 모든 예제는 아래 모델 스키마를 가정합니다: + +```typescript +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + group?: Group & Reference; + + constructor(public username: string) { + } +} +``` + +```typescript +//그룹이 할당된 사용자만 선택 (INNER JOIN) +const users = await session.query(User).innerJoin('group').find(); +for (const user of users) { + user.group; //error, reference가 로드되지 않았기 때문 +} +``` + +```typescript +//그룹이 할당된 사용자만 선택 (INNER JOIN)하고 relation 로드 +const users = await session.query(User).innerJoinWith('group').find(); +for (const user of users) { + user.group.name; //동작함 +} +``` + +조인 쿼리를 수정하려면 동일한 Method에 `use` prefix를 붙여 사용하세요: `useJoin`, `useInnerJoin`, `useJoinWith`, `useInnerJoinWith`. 조인 쿼리 수정을 종료하고 상위 쿼리로 돌아가려면 `end()`를 사용하세요. + +```typescript +//이름이 'admins'인 그룹이 할당된 사용자만 선택 (INNER JOIN) +const users = await session.query(User) + .useInnerJoinWith('group') + .filter({name: 'admins'}) + .end() // 상위 쿼리로 돌아감 + .find(); + +for (const user of users) { + user.group.name; //항상 admin +} +``` + +## 집계 (Aggregation) + +집계 Method를 사용하면 레코드를 count하고 필드를 집계할 수 있습니다. + +다음 예제는 아래 모델 스키마를 가정합니다: + +```typescript +@entity.name('file') +class File { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + downloads: number = 0; + + category: string = 'none'; + + constructor(public path: string & Index) { + } +} +``` + +`groupBy`는 지정된 필드로 결과를 그룹화합니다. + +```typescript +await database.persist( + cast({path: 'file1', category: 'images'}), + cast({path: 'file2', category: 'images'}), + cast({path: 'file3', category: 'pdfs'}) +); + +//[ { category: 'images' }, { category: 'pdfs' } ] +await session.query(File).groupBy('category').find(); +``` + +여러 집계 Method가 있습니다: `withSum`, `withAverage`, `withCount`, `withMin`, `withMax`, `withGroupConcat`. 각각 첫 번째 인자로 필드 이름이 필요하며, 별칭을 변경하기 위한 두 번째 인자는 선택 사항입니다. + +```typescript +// 먼저 일부 레코드를 업데이트해봅시다: +await database.query(File).filter({path: 'images/file1'}).patchOne({$inc: {downloads: 15}}); +await database.query(File).filter({path: 'images/file2'}).patchOne({$inc: {downloads: 5}}); + +//[{ category: 'images', downloads: 20 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withSum('downloads').find(); + +//[{ category: 'images', downloads: 10 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withAverage('downloads').find(); + +//[ { category: 'images', amount: 2 }, { category: 'pdfs', amount: 1 } ] +await session.query(File).groupBy('category').withCount('id', 'amount').find(); +``` + +## Returning + +`patch`와 `delete`를 통해 변경이 있을 때 `returning`으로 추가 필드를 요청할 수 있습니다. + +주의: 모든 데이터베이스 adapter가 필드를 원자적으로 반환하지는 않습니다. 데이터 일관성을 위해 트랜잭션을 사용하세요. + +```typescript +await database.query(User).patchMany({visits: 0}); + +//{ modified: 1, returning: { visits: [ 5 ] }, primaryKeys: [ 1 ] } +const result = await database.query(User) + .filter({username: 'User1'}) + .returning('username', 'visits') + .patchOne({$inc: {visits: 5}}); +``` + +## Find + +지정된 필터와 일치하는 항목의 배열을 반환합니다. + +```typescript +const users: User[] = await database.query(User).filter({username: 'Peter'}).find(); +``` + +## FindOne + +지정된 필터와 일치하는 항목을 반환합니다. +항목을 찾지 못하면 `ItemNotFound` error가 발생합니다. + +```typescript +const users: User = await database.query(User).filter({username: 'Peter'}).findOne(); +``` + +## FindOneOrUndefined + +지정된 필터와 일치하는 항목을 반환합니다. +항목을 찾지 못하면 undefined가 반환됩니다. + +```typescript +const query = database.query(User).filter({username: 'Peter'}); +const users: User|undefined = await query.findOneOrUndefined(); +``` + +## FindField + +지정된 필터와 일치하는 특정 필드의 목록을 반환합니다. + +```typescript +const usernames: string[] = await database.query(User).findField('username'); +``` + +## FindOneField + +지정된 필터와 일치하는 특정 필드의 값을 반환합니다. +항목을 찾지 못하면 `ItemNotFound` error가 발생합니다. + +```typescript +const username: string = await database.query(User).filter({id: 3}).findOneField('username'); +``` + +## Patch + +Patch는 쿼리에 기술된 레코드를 패치하는 변경 쿼리입니다. `patchOne`과 `patchMany`는 쿼리를 종료하고 패치를 실행합니다. + +`patchMany`는 지정된 필터와 일치하는 데이터베이스의 모든 레코드를 변경합니다. 필터가 설정되지 않으면 전체 테이블이 변경됩니다. 한 번에 하나의 항목만 변경하려면 `patchOne`을 사용하세요. + +```typescript +await database.query(User).filter({username: 'Peter'}).patch({username: 'Peter2'}); + +await database.query(User).filter({username: 'User1'}).patchOne({birthdate: new Date}); +await database.query(User).filter({username: 'User1'}).patchOne({$inc: {visits: 1}}); + +await database.query(User).patchMany({visits: 0}); +``` + +## Delete + +`deleteMany`는 지정된 필터와 일치하는 모든 항목을 데이터베이스에서 삭제합니다. +필터가 설정되지 않으면 전체 테이블이 삭제됩니다. 한 번에 하나의 항목만 삭제하려면 `deleteOne`을 사용하세요. + +```typescript +const result = await database.query(User) + .filter({visits: 0}) + .deleteMany(); + +const result = await database.query(User).filter({id: 4}).deleteOne(); +``` + +## Has + +데이터베이스에 최소 한 개의 항목이 존재하는지 여부를 반환합니다. + +```typescript +const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has(); +``` + +## Count + +항목의 개수를 반환합니다. + +```typescript +const userCount: number = await database.query(User).count(); +``` + +## Lift + +쿼리를 Lift한다는 것은 새로운 기능을 추가한다는 의미입니다. 이는 보통 플러그인이나 복잡한 아키텍처에서 더 큰 쿼리 Class를 여러 개의 편리하고 재사용 가능한 Class로 분할하기 위해 사용됩니다. + +```typescript +import { FilterQuery, Query } from '@deepkit/orm'; + +class UserQuery extends Query { + hasBirthday() { + const start = new Date(); + start.setHours(0,0,0,0); + const end = new Date(); + end.setHours(23,59,59,999); + + return this.filter({$and: [{birthdate: {$gte: start}}, {birthdate: {$lte: end}}]} as FilterQuery); + } +} + +await session.query(User).lift(UserQuery).hasBirthday().find(); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/raw-access.md b/website/src/translations/ko/documentation/orm/raw-access.md new file mode 100644 index 000000000..18c2197ef --- /dev/null +++ b/website/src/translations/ko/documentation/orm/raw-access.md @@ -0,0 +1,78 @@ +# Raw 액세스 + +ORM에서 지원하지 않는 SQL query를 실행해야 하는 등, 데이터베이스에 직접 접근해야 할 때가 자주 있습니다. +이는 `Database` Class의 `raw` Method를 사용하여 수행할 수 있습니다. + +```typescript +import { PrimaryKey, AutoIncrement, @entity } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; +import { sql } from '@deepkit/sql'; +import { SqliteDatabaseAdapter } from '@deepkit/sqlite'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + constructor(public username: string) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + +const query = 'Pet%'; +const rows = await database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`).find(); + +const result = await database.raw<{ count: number }>(sql`SELECT count(*) as count FROM users WHERE username LIKE ${query}`).findOne(); +console.log('Found', result.count, 'users'); +``` + +SQL query는 `sql` template string tag를 사용해 구성됩니다. 이는 값을 Parameter로 전달할 수 있게 해주는 특별한 template string tag입니다. 이렇게 전달된 Parameter는 자동으로 파싱되어 안전한 prepared statement로 변환됩니다. 이는 SQL injection 공격을 방지하는 데 중요합니다. + +컬럼 이름과 같은 동적 identifier를 전달하려면 `identifier`를 사용할 수 있습니다: + +```typescript +import { identifier, sql } from '@deepkit/sql'; + +let column = 'username'; +const rows = await database.raw(sql`SELECT * FROM users WHERE ${identifier(column)} LIKE ${query}`).find(); +``` + +SQL adapter의 경우, `raw` Method는 결과를 가져오기 위한 `findOne`, `find` Method를 포함한 `RawQuery`를 반환합니다. UPDATE/DELETE 등과 같이 row를 반환하지 않는 SQL을 실행하려면 `execute`를 사용할 수 있습니다: + +```typescript +let username = 'Peter'; +await database.raw(sql`UPDATE users SET username = ${username} WHERE id = 1`).execute(); +``` + +`RawQuery`는 database adapter에 맞게 올바르게 포맷팅된 최종 SQL string과 Parameter를 가져오는 것도 지원합니다: + +```typescript +const query = database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`); +console.log(query.sql); +console.log(query.params); +``` + +이렇게 하면 예를 들어 다른 database client에서 해당 SQL을 실행하는 데 사용할 수 있습니다. + +## 타입 + +`raw`에 어떤 Type이든 전달할 수 있으며, 데이터베이스에서 반환된 결과는 해당 Type으로 자동 변환됩니다. 이는 특히 SQL adapter에서 유용하며, Class를 전달하면 결과가 해당 Class로 자동 변환됩니다. + +다만 제한이 있습니다. 이 방식으로는 SQL Join이 지원되지 않습니다. Join을 사용하려면 ORM의 query builder를 사용해야 합니다. + +## Mongo + +MongoDB adapter는 SQL query 기반이 아니라 Mongo command 기반이기 때문에 동작 방식이 조금 다릅니다. + +command는 aggregation pipeline, find query, 또는 write command가 될 수 있습니다. + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase')); + +// 첫 번째 Argument는 entry point collection, 두 번째는 command의 Return Type입니다 +const items = await database.raw([ + { $match: { roomId: 'room1' } }, + { $group: { _id: '$userId', count: { $sum: 1 } } }, +]).find(); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/relations.md b/website/src/translations/ko/documentation/orm/relations.md new file mode 100644 index 000000000..442303e45 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/relations.md @@ -0,0 +1,153 @@ +# 관계 + +관계는 두 엔티티를 특정 방식으로 연결할 수 있게 해줍니다. 이는 보통 데이터베이스에서 외래 키(foreign key) 개념을 사용해 수행됩니다. Deepkit ORM은 모든 공식 데이터베이스 어댑터에서 관계를 지원합니다. + +관계는 `Reference` 데코레이터로 주석 처리됩니다. 일반적으로 관계에는 반대 방향의 관계도 있는데, 이는 `BackReference` 타입으로 주석 처리되며, 데이터베이스 쿼리에서 역방향 관계를 사용하려는 경우에만 필요합니다. Back reference는 오직 가상입니다. + +## 일대다 + +참조를 저장하는 엔티티는 보통 `owning side` 또는 참조를 `owns`하는 쪽이라고 부릅니다. 다음 코드는 `User`와 `Post` 사이의 일대다 관계를 가진 두 엔티티를 보여줍니다. 이는 하나의 `User`가 여러 개의 `Post`를 가질 수 있음을 의미합니다. `post` 엔티티에는 `post->user` 관계가 있습니다. 데이터베이스 자체에는 이제 `User`의 기본 키를 담고 있는 `Post."author"` 필드가 생깁니다. + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement, + Reference } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.collection('posts') +class Post { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor( + public author: User & Reference, + public title: string + ) { + } +} + +const database = new Database( + new SQLiteDatabaseAdapter(':memory:'), + [User, Post] +); +await database.migrate(); + +const user1 = new User('User1'); +const post1 = new Post(user1, 'My first blog post'); +const post2 = new Post(user1, 'My second blog post'); + +await database.persist(user1, post1, post2); +``` + +기본적으로 쿼리에서 Reference는 자동으로 선택되지 않습니다. 자세한 내용은 [데이터베이스 Join](./query.md#join)을 참고하세요. + +## 다대일 + +참조에는 일반적으로 다대일이라고 불리는 역참조가 있습니다. 이는 데이터베이스 자체에 반영되지 않기 때문에 오직 가상 참조입니다. Back reference는 `BackReference`로 주석 처리되며, 주로 리플렉션과 쿼리 join에 사용됩니다. `User`에서 `Post`로 `BackReference`를 추가하면, `User` 쿼리에서 직접 `Post`를 join할 수 있습니다. + +```typescript +@entity.name('user').collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + posts?: Post[] & BackReference; + + constructor(public username: string) { + } +} +``` + +```typescript +//[ { username: 'User1', posts: [ [Post], [Post] ] } ] +const users = await database.query(User) + .select('username', 'posts') + .joinWith('posts') + .find(); +``` + +## 다대다 + +다대다 관계는 많은 레코드를 다른 많은 레코드와 연결할 수 있게 해줍니다. 예를 들어, 그룹에 속한 사용자에 사용할 수 있습니다. 한 사용자는 0개, 1개 또는 여러 개의 그룹에 있을 수 있습니다. 따라서 한 그룹은 0명, 1명 또는 여러 명의 사용자를 포함할 수 있습니다. + +다대다 관계는 보통 피벗 엔티티를 사용하여 구현합니다. 피벗 엔티티는 두 다른 엔티티에 대한 실제 own reference들을 포함하고, 이 두 엔티티는 피벗 엔티티에 대한 back reference를 가집니다. + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + groups?: Group[] & BackReference<{via: typeof UserGroup}>; + + constructor(public username: string) { + } +} + +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + + users?: User[] & BackReference<{via: typeof UserGroup}>; + + constructor(public name: string) { + } +} + +// 피벗 엔티티 +@entity.name('userGroup') +class UserGroup { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public user: User & Reference, + public group: Group & Reference, + ) { + } +} +``` + +이 엔티티들을 사용하여 이제 사용자와 그룹을 생성하고 피벗 엔티티로 연결할 수 있습니다. User에 back reference를 사용함으로써, User 쿼리로 그룹들을 직접 조회할 수 있습니다. + +```typescript +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Group, UserGroup]); +await database.migrate(); + +const user1 = new User('User1'); +const user2 = new User('User2'); +const group1 = new Group('Group1'); + +await database.persist(user1, user2, group1, new UserGroup(user1, group1), new UserGroup(user2, group1)); + +//[ +// { id: 1, username: 'User1', groups: [ [Group] ] }, +// { id: 2, username: 'User2', groups: [ [Group] ] } +// ] +const users = await database.query(User) + .select('username', 'groups') + .joinWith('groups') + .find(); +``` + +사용자를 그룹에서 분리(unlink)하려면, 해당 UserGroup 레코드를 삭제합니다: + +```typescript +const users = await database.query(UserGroup) + .filter({user: user1, group: group1}) + .deleteOne(); +``` + +## 일대일 + +## 제약 조건 + +삭제/업데이트 시: RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/seeding.md b/website/src/translations/ko/documentation/orm/seeding.md new file mode 100644 index 000000000..89311a765 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/seeding.md @@ -0,0 +1,3 @@ +# 시딩 + +데이터베이스 시딩은 데이터로 데이터베이스를 초기 적재하는 작업입니다. 이는 개발, 테스트, 또는 프로덕션 데이터베이스 초기화 시 사용하도록 의도된 초기 데이터베이스 적재의 한 형태입니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/session.md b/website/src/translations/ko/documentation/orm/session.md new file mode 100644 index 000000000..4ead75f5a --- /dev/null +++ b/website/src/translations/ko/documentation/orm/session.md @@ -0,0 +1,59 @@ +# 세션 / Unit Of Work + +세션은 일종의 Unit Of Work와 같습니다. `commit()`이 호출될 때마다 당신이 하는 모든 작업을 추적하고 변경 사항을 자동으로 기록합니다. 세션은 여러 쿼리를 묶어 매우 빠르게 처리하기 때문에 데이터베이스에 변경을 수행하는 데 선호되는 방식입니다. 세션은 매우 가벼워서 예를 들어 request-response 라이프사이클에서 쉽게 생성할 수 있습니다. + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + + @entity.name('user') + class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + await database.migrate(); + + const session = database.createSession(); + session.add(new User('User1'), new User('User2'), new User('User3')); + + await session.commit(); + + const users = await session.query(User).find(); + console.log(users); +} + +main(); +``` + +`session.add(T)`로 세션에 새 인스턴스를 추가하거나 `session.remove(T)`로 기존 인스턴스를 제거하세요. Session 객체 사용을 마쳤다면, 가비지 컬렉터가 제거할 수 있도록 모든 곳에서 참조를 해제(dereference)하면 됩니다. + +Session 객체를 통해 가져온 entity 인스턴스의 변경 사항은 자동으로 감지됩니다. + +```typescript +const users = await session.query(User).find(); +for (const user of users) { + user.name += ' changed'; +} + +await session.commit();//모든 사용자를 저장합니다 +``` + +## Identity Map + +세션은 데이터베이스 엔트리마다 단 하나의 JavaScript 객체만 존재하도록 보장하는 Identity Map을 제공합니다. 예를 들어, 동일한 세션 내에서 `session.query(User).find()`를 두 번 실행하면 두 개의 서로 다른 배열을 받지만, 그 안에는 동일한 entity 인스턴스들이 들어 있습니다. + +`session.add(entity1)`로 새로운 entity를 추가하고 다시 조회하면 정확히 동일한 entity 인스턴스 `entity1`을 얻게 됩니다. + +중요: 세션을 사용하기 시작하면 `database.query` 대신 `session.query` Method를 사용해야 합니다. Identity mapping 기능은 세션 쿼리에서만 활성화됩니다. + +## 변경 감지 + +## Request/Response \ No newline at end of file diff --git a/website/src/translations/ko/documentation/orm/transactions.md b/website/src/translations/ko/documentation/orm/transactions.md new file mode 100644 index 000000000..3ecac9184 --- /dev/null +++ b/website/src/translations/ko/documentation/orm/transactions.md @@ -0,0 +1,85 @@ +# 트랜잭션 + +트랜잭션은 select, insert, update, delete와 같은 statement, query, operation을 하나의 작업 단위로 순차적으로 실행하며, 이 단위는 commit 또는 rollback할 수 있습니다. + +Deepkit은 공식적으로 지원되는 모든 데이터베이스에 대해 트랜잭션을 지원합니다. 기본적으로 어떤 query나 database session에도 트랜잭션은 사용되지 않습니다. 트랜잭션을 활성화하려면 두 가지 주요 방법이 있습니다: sessions와 callback. + +## 세션 트랜잭션 + +생성하는 각 session마다 새로운 트랜잭션을 시작하고 할당할 수 있습니다. 이는 데이터베이스와 상호작용하는 선호되는 방식으로, Session 객체를 쉽게 전달할 수 있고 해당 session에 의해 생성된 모든 query가 자동으로 그 트랜잭션에 할당됩니다. + +일반적인 패턴은 모든 작업을 try-catch 블록으로 감싸고 맨 마지막 줄에서 `commit()`을 실행하는 것입니다(이는 이전 명령이 모두 성공했을 때만 실행됩니다). 그리고 에러가 발생하는 즉시 모든 변경 사항을 되돌리기 위해 catch 블록에서 `rollback()`을 실행합니다. + +대체 API(아래 참조)가 있긴 하지만, 모든 트랜잭션은 데이터베이스 session 객체로만 동작합니다. 데이터베이스 session의 unit-of-work에서 열린 변경 사항을 데이터베이스에 커밋하려면 보통 `commit()`을 호출합니다. 트랜잭션 session에서 `commit()`은 보류 중인 모든 변경 사항을 데이터베이스에 커밋할 뿐만 아니라 트랜잭션 자체도 완료("commits")하여 트랜잭션을 종료합니다. 또는 `session.flush()`를 호출해 `commit` 없이, 따라서 트랜잭션을 닫지 않고, 보류 중인 모든 변경 사항을 커밋할 수 있습니다. unit-of-work를 flush하지 않고 트랜잭션만 커밋하려면 `session.commitTransaction()`을 사용하세요. + +```typescript +const session = database.createSession(); + +//이 작업은 새 transaction을 할당하고, 바로 다음 database operation부터 시작합니다. +session.useTransaction(); + +try { + //이 query는 트랜잭션 내에서 실행됩니다 + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); + + await session.commit(); +} catch (error) { + await session.rollback(); +} +``` + +session에서 `commit()` 또는 `rollback()`이 실행되면 트랜잭션은 해제됩니다. 새로운 트랜잭션으로 계속하려면 `useTransaction()`을 다시 호출해야 합니다. + +트랜잭션 session에서 첫 번째 database operation이 실행되는 순간, 할당된 database connection은 현재 session 객체에 고정되고 독점적이 됩니다(sticky). 따라서 이후의 모든 작업은 동일한 connection에서 수행됩니다(즉, 대부분의 데이터베이스에서 동일한 데이터베이스 서버). 트랜잭션 session이 종료될 때(commit 또는 rollback)만 database connection이 다시 해제됩니다. 그러므로 트랜잭션은 필요한 만큼만 짧게 유지하는 것이 좋습니다. + +session이 이미 트랜잭션에 연결되어 있다면, `session.useTransaction()` 호출은 항상 동일한 객체를 반환합니다. session에 트랜잭션이 연결되어 있는지 확인하려면 `session.isTransaction()`을 사용하세요. + +중첩 트랜잭션은 지원되지 않습니다. + +## 트랜잭션 Callback + +트랜잭션 session의 대안은 `database.transaction(callback)`입니다. + +```typescript +await database.transaction(async (session) => { + //이 query는 트랜잭션 내에서 실행됩니다 + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); +}); +``` + +`database.transaction(callback)` 메서드는 새로운 트랜잭션 session 내에서 비동기 callback을 수행합니다. callback이 성공하면(즉, 에러가 발생하지 않으면) session은 자동으로 커밋됩니다(따라서 해당 트랜잭션이 커밋되고 모든 변경 사항이 flush됩니다). callback이 실패하면 session은 자동으로 `rollback()`을 실행하고 에러를 전파합니다. + +## 격리 수준 + +많은 데이터베이스는 다양한 종류의 트랜잭션을 지원합니다. 트랜잭션 동작을 변경하려면 `useTransaction()`에서 반환된 트랜잭션 객체에 대해 다양한 메서드를 호출할 수 있습니다. 이 트랜잭션 객체의 인터페이스는 사용하는 database adapter에 따라 다릅니다. 예를 들어, MySQL 데이터베이스에서 반환된 트랜잭션 객체는 MongoDB 데이터베이스에서 반환된 것과 다른 옵션을 가집니다. 가능한 옵션 목록을 얻으려면 code completion을 사용하거나 database adapter의 인터페이스를 확인하세요. + +```typescript +const database = new Database(new MySQLDatabaseAdapter()); + +const session = database.createSession(); +session.useTransaction().readUncommitted(); + +try { + //...작업 + await session.commit(); +} catch (error) { + await session.rollback(); +} + +//또는 +await database.transaction(async (session) => { + //아직 어떤 database operation도 실행되지 않았다면 동작합니다. + session.useTransaction().readUncommitted(); + + //...작업 +}); +``` + +MySQL, PostgreSQL 및 SQLite의 트랜잭션은 기본적으로 동작하지만, MongoDB는 먼저 "replica set"으로 설정해야 합니다. + +표준 MongoDB 인스턴스를 replica set으로 변환하려면 다음 공식 문서를 참고하세요: +[Standalone을 Replica Set으로 변환](https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set). \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/angular-ssr.md b/website/src/translations/ko/documentation/package/angular-ssr.md new file mode 100644 index 000000000..8451accbd --- /dev/null +++ b/website/src/translations/ko/documentation/package/angular-ssr.md @@ -0,0 +1,128 @@ +# API `@deepkit/angular-ssr` + +```shell +npm install @deepkit/angular-ssr +``` + + +- 메인 애플리케이션을 `app.ts` 안에 두고 `angular.json`에서 설정하세요: + +`src/server/app.ts`에서: + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { AngularModule, RequestHandler } from '@deepkit/angular-ssr'; + +const app = new App({ + controllers: [ + // 사용자 컨트롤러 + ], + providers: [ + // 사용자 프로바이더 + ], + imports: [ + new FrameworkModule({}), + new AngularModule({ + moduleUrl: import.meta.url, + }) + ] +}); + +const main = isMainModule(import.meta.url); + +if (main) { + void app.run(); // server:start를 포함한 모든 CLI 명령을 호출할 수 있게 해줍니다 +} + +export const reqHandler = main + // 메인에서 실행 중일 때는 새 request handler를 생성하지 않습니다 + ? () => undefined + : app.get(RequestHandler).create(); +``` + +```json +{ + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/app", + "index": "src/index.html", + "server": "src/main.server.ts", + "outputMode": "server", + "ssr": { + "entry": "src/server/app.ts" + }, + "browser": "src/main.ts" + } +} +``` + +`src/server/app.ts`도 tsconfig에 포함되어 있는지 확인하세요. + +## Angular App 구성 + +`app/app.config.ts`(클라이언트 측)에서: + +```typescript +@Injectable() +export class APIInterceptor implements HttpInterceptor { + constructor(@Inject('baseUrl') @Optional() private baseUrl: string) { + // 클라이언트 빌드에서는 `baseUrl`이 비어 있으며, 현재 location에서 유추되어야 합니다. + // 이것이 올바르지 않다면, `appConfig` 객체의 `providers` 배열에서 `baseUrl`을 간단히 정의할 수 있습니다. + this.baseUrl = baseUrl || (typeof location !== 'undefined' ? location.origin : ''); + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const apiReq = req.clone({ url: `${this.baseUrl}/${req.url}` }); + return next.handle(apiReq); + } +} + +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: APIInterceptor, + multi: true, + }, + // 기타 프로바이더 + ], +}; +``` + +`app/app.server.config.ts`(서버 측)에서: + +```typescript +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering(), + provideServerRouting([ + { + path: '**', + renderMode: RenderMode.Server, + }, + ]), + { + provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP, + deps: [REQUEST_CONTEXT], + useFactory(context: any) { + return { [context?.baseUrl]: context?.publicBaseUrl || '' }; + }, + }, + { + provide: 'baseUrl', + deps: [REQUEST_CONTEXT], + useFactory: (context: any) => { + return context?.baseUrl || ''; + }, + } + ], +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); +``` + + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/api-console.md b/website/src/translations/ko/documentation/package/api-console.md new file mode 100644 index 000000000..be3cb0bf1 --- /dev/null +++ b/website/src/translations/ko/documentation/package/api-console.md @@ -0,0 +1,40 @@ +# Deepkit API Console + +```bash +npms install @deepkit/api-console-module +``` + +HTTP 및 RPC API의 모든 routes, actions, parameters, return types, status codes를 TypeScript type syntax로 보여주는 자동 문서화. + +이는 [Framework Debugger](../framework.md)의 일부이지만 독립적으로도 사용할 수 있습니다. + +```typescript +import { ApiConsoleModule } from '@deepkit/api-console-module'; + +new App({ + imports: [ + new ApiConsoleModule({ + path: '/api', + markdown: ` + # My API + + This is my API documentation. + + Have fun! + ` + }), + ] +}) +``` + +기본적으로 `new ApiConsoleModule`은 모든 HTTP 및 RPC routes를 보여줍니다. 또한 `ApiConsoleModule` Class의 Method들을 사용하여 어떤 routes를 표시할지 지정할 수도 있습니다. + + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/app.md b/website/src/translations/ko/documentation/package/app.md new file mode 100644 index 000000000..e728c2c4b --- /dev/null +++ b/website/src/translations/ko/documentation/package/app.md @@ -0,0 +1,14 @@ +# API `@deepkit/app` + +```shell +npm install @deepkit/app +``` + +command-line interface (CLI) parser, 서비스 컨테이너와 DI (Dependency Injection), 이벤트 디스패처, 앱 모듈 시스템, configuration loader를 제공합니다. + +이것은 Deepkit 애플리케이션을 작성하기 위한 코어입니다. +CLI 컨트롤러, HTTP 컨트롤러 또는 라우트, RPC 컨트롤러([framework 모듈](../framework.md)을 통해), 그리고 기타 서비스를 등록할 수 있습니다. + +자세한 내용은 [Deepkit App 문서](../app.md)를 참조하세요. + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/bench.md b/website/src/translations/ko/documentation/package/bench.md new file mode 100644 index 000000000..d4311dcc7 --- /dev/null +++ b/website/src/translations/ko/documentation/package/bench.md @@ -0,0 +1,35 @@ +# API `@deepkit/bench` + +```sh +npm install @deepkit/bench +``` + +코드 스니펫을 벤치마크하기 위한 간단한 도구입니다. + +```typescript +import { benchmark, run } from '@deepkit/bench'; + +// ASCII 바이너리 파싱 예제 +const binaryString = Buffer.from('Hello World', 'utf8'); +const codes = [ + +benchmark('Buffer.toString', () => { + const utf8String = binaryString.toString('utf8'); +}); + +benchmark('String.fromCodePoint', () => { + const utf8String = String.fromCodePoint() +}); + +void run(); +``` + +```sh +$ node --import @deepkit/run benchmarks/ascii-parsing.ts +Node v22.13.1 + 🏎 x 20,326,482.53 ops/sec ± 4.95% 0.000049 ms/op ▆▆▇▅▆▆▅▅▆▅▅▅▅▅▅▅▅▅▅▅▅▅ Buffer.toString 19850001 samples + 🏎 x 36,012,545.69 ops/sec ± 1.78% 0.000028 ms/op ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ String.fromCodePoint 35800001 samples +done +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/broker-redis.md b/website/src/translations/ko/documentation/package/broker-redis.md new file mode 100644 index 000000000..c4bf0bfaf --- /dev/null +++ b/website/src/translations/ko/documentation/package/broker-redis.md @@ -0,0 +1,29 @@ +# API `@deepkit/broker-redis` + +```sh +npm install @deepkit/broker-redis +``` + +Deepkit Broker의 Redis 기반 구현을 제공합니다. 내부적으로 ioredis를 사용합니다. + +이 adapter는 Deepkit Broker의 queue adapter를 구현하지 않습니다. + +```typescript +import { BrokerKeyValue, BrokerBus } from '@deepkit/broker'; +import { BrokerRedisAdapter } from '@deepkit/broker-redis'; +import { ConsoleLogger } from '@deepkit/logger'; + +const adapter = new RedisBrokerAdapter({ + preifx: 'myapp:', + host: 'localhost', + port: 6379, + // password: 'your-password', // 선택 사항, Redis 서버에서 인증이 필요한 경우 + // db: 0, // 선택 사항, 다른 Redis 데이터베이스를 지정하려는 경우 +}, new ConsoleLogger()); + +const keyValye = new BrokerKeyValue(adapter); +const bus = new BrokerBus(adapter); +// ... +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/broker.md b/website/src/translations/ko/documentation/package/broker.md new file mode 100644 index 000000000..00d9bb7c0 --- /dev/null +++ b/website/src/translations/ko/documentation/package/broker.md @@ -0,0 +1,9 @@ +# API `@deepkit/broker` + +```sh +npm install @deepkit/broker +``` + +이 패키지는 Deepkit Broker의 추상화와 서버 구현, 그리고 adapter로서의 여러 클라이언트 구현(`BrokerDeepkitAdapter`, `BrokerMemoryAdapter`)을 포함합니다. + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/bson.md b/website/src/translations/ko/documentation/package/bson.md new file mode 100644 index 000000000..3fdda75b0 --- /dev/null +++ b/website/src/translations/ko/documentation/package/bson.md @@ -0,0 +1,9 @@ +# API `@deepkit/bson` + +```shell +npm install @deepkit/bson +``` + +BSON과 JavaScript 객체 간 변환을 위한 인코더/디코더 함수를 제공합니다. + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/bun.md b/website/src/translations/ko/documentation/package/bun.md new file mode 100644 index 000000000..ebc847bad --- /dev/null +++ b/website/src/translations/ko/documentation/package/bun.md @@ -0,0 +1,7 @@ +# API `@deepkit/bun` + +```shell +npm install @deepkit/bun +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/core-rxjs.md b/website/src/translations/ko/documentation/package/core-rxjs.md new file mode 100644 index 000000000..bca3c9544 --- /dev/null +++ b/website/src/translations/ko/documentation/package/core-rxjs.md @@ -0,0 +1,7 @@ +# API `@deepkit/core-rxjs` + +```shell +npm install @deepkit/core-rxjs +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/core.md b/website/src/translations/ko/documentation/package/core.md new file mode 100644 index 000000000..b0e2ccdf0 --- /dev/null +++ b/website/src/translations/ko/documentation/package/core.md @@ -0,0 +1,7 @@ +# API `@deepkit/core` + +```shell +npm install @deepkit/core +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/devtool.md b/website/src/translations/ko/documentation/package/devtool.md new file mode 100644 index 000000000..c9b744bd3 --- /dev/null +++ b/website/src/translations/ko/documentation/package/devtool.md @@ -0,0 +1,5 @@ +# 개발자 도구 + +Deepkit Devtool은 Chrome 개발자 도구에서 RPC 연결을 디버깅할 수 있게 해주는 Chrome 확장 프로그램입니다. + +[Deepkit Devtool](https://chromewebstore.google.com/detail/deepkit-devtool/lkncgbbafldohehlfdnkflbeapckdnlj) \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/event.md b/website/src/translations/ko/documentation/package/event.md new file mode 100644 index 000000000..ae9066e20 --- /dev/null +++ b/website/src/translations/ko/documentation/package/event.md @@ -0,0 +1,9 @@ +# API `@deepkit/event` + +```shell +npm install @deepkit/event +``` + +애플리케이션에서 이벤트를 사용하는 방법에 대한 자세한 내용은 [앱 이벤트](../app/events.md)를 참조하세요. + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/filesystem-aws-s3.md b/website/src/translations/ko/documentation/package/filesystem-aws-s3.md new file mode 100644 index 000000000..942173a3b --- /dev/null +++ b/website/src/translations/ko/documentation/package/filesystem-aws-s3.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-aws-s3` + +```shell +npm install @deepkit/filesystem-aws-s3 +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/filesystem-database.md b/website/src/translations/ko/documentation/package/filesystem-database.md new file mode 100644 index 000000000..71a71fdd8 --- /dev/null +++ b/website/src/translations/ko/documentation/package/filesystem-database.md @@ -0,0 +1,9 @@ +# API `@deepkit/filesystem-database` + +```shell +npm install @deepkit/filesystem-database +``` + +모든 지원되는 Deepkit ORM 데이터베이스를 파일 시스템 백엔드로 사용할 수 있게 해줍니다. + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/filesystem-ftp.md b/website/src/translations/ko/documentation/package/filesystem-ftp.md new file mode 100644 index 000000000..d01521c0a --- /dev/null +++ b/website/src/translations/ko/documentation/package/filesystem-ftp.md @@ -0,0 +1,8 @@ +# API `@deepkit/filesystem-ftp` + +```shell +npm install @deepkit/filesystem-ftp +``` + + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/filesystem-google.md b/website/src/translations/ko/documentation/package/filesystem-google.md new file mode 100644 index 000000000..c7af80585 --- /dev/null +++ b/website/src/translations/ko/documentation/package/filesystem-google.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-google` + +```shell +npm install @deepkit/filesystem-google +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/filesystem-sftp.md b/website/src/translations/ko/documentation/package/filesystem-sftp.md new file mode 100644 index 000000000..719ebfd37 --- /dev/null +++ b/website/src/translations/ko/documentation/package/filesystem-sftp.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-sftp` + +```shell +npm install @deepkit/filesystem-sftp +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/filesystem.md b/website/src/translations/ko/documentation/package/filesystem.md new file mode 100644 index 000000000..68cc87b21 --- /dev/null +++ b/website/src/translations/ko/documentation/package/filesystem.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem` + +```shell +npm install @deepkit/filesystem +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/framework.md b/website/src/translations/ko/documentation/package/framework.md new file mode 100644 index 000000000..609d37e4b --- /dev/null +++ b/website/src/translations/ko/documentation/package/framework.md @@ -0,0 +1,7 @@ +# API `@deepkit/framework` + +```shell +npm install @deepkit/framework +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/http.md b/website/src/translations/ko/documentation/package/http.md new file mode 100644 index 000000000..f96adb231 --- /dev/null +++ b/website/src/translations/ko/documentation/package/http.md @@ -0,0 +1,8 @@ +# API `@deepkit/http` + +```shell +npm install @deepkit/http +``` + + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/injector.md b/website/src/translations/ko/documentation/package/injector.md new file mode 100644 index 000000000..b7b6febab --- /dev/null +++ b/website/src/translations/ko/documentation/package/injector.md @@ -0,0 +1,7 @@ +# API `@deepkit/injector` + +```shell +npm install @deepkit/injector +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/logger.md b/website/src/translations/ko/documentation/package/logger.md new file mode 100644 index 000000000..224a2be7c --- /dev/null +++ b/website/src/translations/ko/documentation/package/logger.md @@ -0,0 +1,7 @@ +# API `@deepkit/logger` + +```sh +npm install @deepkit/logger +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/mongo.md b/website/src/translations/ko/documentation/package/mongo.md new file mode 100644 index 000000000..151733ee8 --- /dev/null +++ b/website/src/translations/ko/documentation/package/mongo.md @@ -0,0 +1,18 @@ +# API `@deepkit/mongo` + +```shell +npm install @deepkit/mongo +``` + +Deepkit ORM용 독립형 MongoDB 드라이버와 데이터베이스 어댑터입니다. + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/mysql.md b/website/src/translations/ko/documentation/package/mysql.md new file mode 100644 index 000000000..a972f372a --- /dev/null +++ b/website/src/translations/ko/documentation/package/mysql.md @@ -0,0 +1,17 @@ +# API `@deepkit/mysql` + +```shell +npm install @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('mysql://user:password@localhost/mydatabase'); +// const adapter = new MySQLDatabaseAdapter({host: 'localhost', port: 3306}); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/orm-browser.md b/website/src/translations/ko/documentation/package/orm-browser.md new file mode 100644 index 000000000..66cba07b9 --- /dev/null +++ b/website/src/translations/ko/documentation/package/orm-browser.md @@ -0,0 +1,18 @@ +# Deepkit ORM Browser + +```sh +npm install @deepkit/orm-browser +``` + +Deepkit ORM Browser는 데이터베이스 ORM 스키마를 탐색하고, 콘텐츠를 편집하며, migration 변경 사항을 확인하고, 데이터베이스를 seeding 할 수 있게 해주는 웹 애플리케이션입니다. + +이는 [Framework Debugger](../framework.md)의 일부이지만, 단독으로도 사용할 수 있습니다. + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/orm.md b/website/src/translations/ko/documentation/package/orm.md new file mode 100644 index 000000000..8ee0b58af --- /dev/null +++ b/website/src/translations/ko/documentation/package/orm.md @@ -0,0 +1,7 @@ +# API `@deepkit/orm` + +```shell +npm install @deepkit/orm +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/postgres.md b/website/src/translations/ko/documentation/package/postgres.md new file mode 100644 index 000000000..3f51e1f54 --- /dev/null +++ b/website/src/translations/ko/documentation/package/postgres.md @@ -0,0 +1,18 @@ +# API `@deepkit/postgres` + +```shell +npm install @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('postgres://user:password@localhost/mydatabase'); +// const adapter = new PostgresDatabaseAdapter({ host: 'localhost', database: 'postgres', user: 'postgres' }); + +const database = new Database(adapter); +``` + + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/rpc-tcp.md b/website/src/translations/ko/documentation/package/rpc-tcp.md new file mode 100644 index 000000000..7a980d719 --- /dev/null +++ b/website/src/translations/ko/documentation/package/rpc-tcp.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc-tcp` + +```shell +npm install @deepkit/rpc-tcp +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/rpc.md b/website/src/translations/ko/documentation/package/rpc.md new file mode 100644 index 000000000..a583f5fe4 --- /dev/null +++ b/website/src/translations/ko/documentation/package/rpc.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc` + +```shell +npm install @deepkit/rpc +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/run.md b/website/src/translations/ko/documentation/package/run.md new file mode 100644 index 000000000..abd2609b1 --- /dev/null +++ b/website/src/translations/ko/documentation/package/run.md @@ -0,0 +1,21 @@ +# API `@deepkit/run` + +```sh +npm install @deepkit/run +``` + +빌드 단계가 필요 없이 TypeScript 코드를 실행하는 간단한 방법입니다. + +이 도구는 주로 Deepkit의 자체 테스트 스위트에서 사용하도록 설계되었지만, 여러분의 프로젝트에서도 사용할 수 있습니다. + +```typescript +import { typeOf } from '@deepkit/type'; + +console.log(typeOf()); +``` + +```sh +node --import @deepkit/run test.ts +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/sql.md b/website/src/translations/ko/documentation/package/sql.md new file mode 100644 index 000000000..3207c73ed --- /dev/null +++ b/website/src/translations/ko/documentation/package/sql.md @@ -0,0 +1,7 @@ +# API `@deepkit/sql` + +```shell +npm install @deepkit/sql +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/sqlite.md b/website/src/translations/ko/documentation/package/sqlite.md new file mode 100644 index 000000000..5486e2c82 --- /dev/null +++ b/website/src/translations/ko/documentation/package/sqlite.md @@ -0,0 +1,16 @@ +# API `@deepkit/sqlite` + +```shell +npm install @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { Database } from '@deepkit/orm'; + +const adapter = new SQLiteDatabaseAdapter(':memory'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/stopwatch.md b/website/src/translations/ko/documentation/package/stopwatch.md new file mode 100644 index 000000000..d2a69ed13 --- /dev/null +++ b/website/src/translations/ko/documentation/package/stopwatch.md @@ -0,0 +1,7 @@ +# API `@deepkit/stopwatch` + +```shell +npm install @deepkit/stopwatch +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/template.md b/website/src/translations/ko/documentation/package/template.md new file mode 100644 index 000000000..e6d908c2b --- /dev/null +++ b/website/src/translations/ko/documentation/package/template.md @@ -0,0 +1,7 @@ +# API `@deepkit/template` + +```shell +npm install @deepkit/template +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/topsort.md b/website/src/translations/ko/documentation/package/topsort.md new file mode 100644 index 000000000..cbeddd4c2 --- /dev/null +++ b/website/src/translations/ko/documentation/package/topsort.md @@ -0,0 +1,7 @@ +# API `@deepkit/topsort` + +```shell +npm install @deepkit/topsort +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/type-compiler.md b/website/src/translations/ko/documentation/package/type-compiler.md new file mode 100644 index 000000000..2b6e11ac0 --- /dev/null +++ b/website/src/translations/ko/documentation/package/type-compiler.md @@ -0,0 +1,9 @@ +# API `@deepkit/type-compiler` + +```shell +npm install @deepkit/type-compiler +``` + +런타임 타입 정보를 런타임에서 사용할 수 있도록 하는 TypeScript transformer. + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/type.md b/website/src/translations/ko/documentation/package/type.md new file mode 100644 index 000000000..195e08585 --- /dev/null +++ b/website/src/translations/ko/documentation/package/type.md @@ -0,0 +1,7 @@ +# API `@deepkit/type` + +```shell +npm install @deepkit/type +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/vite.md b/website/src/translations/ko/documentation/package/vite.md new file mode 100644 index 000000000..3f1bcdd0a --- /dev/null +++ b/website/src/translations/ko/documentation/package/vite.md @@ -0,0 +1,7 @@ +# API `@deepkit/vite` + +```shell +npm install @deepkit/vite +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/package/workflow.md b/website/src/translations/ko/documentation/package/workflow.md new file mode 100644 index 000000000..ff27914f7 --- /dev/null +++ b/website/src/translations/ko/documentation/package/workflow.md @@ -0,0 +1,7 @@ +# API `@deepkit/workflow` + +```shell +npm install @deepkit/workflow +``` + + \ No newline at end of file diff --git a/website/src/translations/ko/documentation/rpc.md b/website/src/translations/ko/documentation/rpc.md new file mode 100644 index 000000000..a3baba8ad --- /dev/null +++ b/website/src/translations/ko/documentation/rpc.md @@ -0,0 +1,64 @@ +# RPC + +RPC(Remote Procedure Call)은 원격 서버의 함수를 로컬 함수처럼 호출할 수 있게 해준다. HTTP client-server 통신이 매핑을 위해 HTTP method와 URL을 사용하는 것과 달리, RPC는 매핑에 함수 이름을 사용한다. 전송할 데이터는 일반 함수 argument로 전달되며, 서버에서의 함수 호출 결과가 client로 되돌아온다. + +RPC의 장점은 header, URL, query string 등과 함께 동작하지 않기 때문에 client-server 추상이 가볍다는 점이다. 단점은 RPC를 통해 서버의 함수를 브라우저에서 쉽게 호출하기 어렵고, 종종 특정 client가 필요하다는 점이다. + +RPC의 핵심 기능 중 하나는 client와 server 사이의 데이터가 자동으로 serialize/deserialize된다는 것이다. 따라서 typesafe한 RPC client가 보통 가능하다. 일부 RPC 프레임워크는 사용자가 특정 형식으로 type(parameter type과 return type)을 제공하도록 강제한다. 이는 gRPC의 Protocol Buffers나 GraphQL 같은 DSL, 또는 JavaScript schema builder 형태일 수 있다. 추가적인 데이터 validation을 RPC 프레임워크에서 제공할 수 있지만, 모든 프레임워크가 지원하는 것은 아니다. + +Deepkit RPC는 TypeScript 코드 자체에서 type을 추출하므로, code generator를 사용하거나 수동으로 정의할 필요가 없다. Deepkit은 parameter와 result의 자동 serialize/deserialize를 지원한다. Validation에서 추가 제약을 정의하면 자동으로 검증된다. 이는 RPC를 통한 통신을 매우 typesafe하고 효율적으로 만든다. Deepkit RPC의 `rxjs`를 통한 streaming 지원은 이 RPC 프레임워크를 실시간 통신에 적합한 도구로 만들어준다. + +RPC의 개념을 설명하기 위해 다음 코드를 살펴보자: + +```typescript +//server.ts +class Controller { + hello(title: string): string { + return 'Hello ' + title + } +} +``` + +hello 같은 method는 서버의 class 내부에서 일반 함수처럼 구현되며, 원격 client가 호출할 수 있다. + +```typescript +//client.ts +const client = new RpcClient('localhost'); +const controller = client.controller(); + +const result = await controller.hello('World'); // => 'Hello World'; +``` + +RPC는 근본적으로 비동기 통신을 기반으로 하므로, 통신은 일반적으로 HTTP를 통해 이루어지지만 TCP나 WebSocket을 사용할 수도 있다. 이는 TypeScript의 모든 함수 호출이 자체적으로 `Promise`로 변환됨을 의미한다. 결과는 해당 `await`로 비동기적으로 받을 수 있다. + +## Isomorphic TypeScript + +프로젝트가 client(보통 frontend)와 server(backend) 모두에서 TypeScript를 사용할 때 이를 Isomorphic TypeScript라고 한다. TypeScript의 type에 기반한 typesafe RPC 프레임워크는 이러한 프로젝트에서 특히 유용한데, client와 server 사이에 type을 공유할 수 있기 때문이다. + +이를 활용하기 위해 양쪽에서 사용하는 type은 별도의 파일이나 package로 분리하는 것이 좋다. 각 측에서 import하면 다시 함께 사용할 수 있다. + +```typescript +//shared.ts +export class User { + id: number; + username: string; +} + +//server.ts +import { User } from './shared'; + +@rpc.controller('/user') +class UserController { + async getUser(id: number): Promise { + return await datbase.query(User).filter({id}).findOne(); + } +} + +//client.ts +import { UserControllerApi } from './shared'; +import type { UserController } from './server.ts' +const controller = client.controller('/user'); +const user = await controller.getUser(2); // => User +``` + +하위 호환성은 일반적인 로컬 API와 동일한 방식으로 구현할 수 있다. 새로운 parameter를 optional로 표시하거나, 새로운 method를 추가하면 된다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/rpc/dependency-injection.md b/website/src/translations/ko/documentation/rpc/dependency-injection.md new file mode 100644 index 000000000..4dba83856 --- /dev/null +++ b/website/src/translations/ko/documentation/rpc/dependency-injection.md @@ -0,0 +1,55 @@ +# Dependency Injection + +Controller 클래스는 `@deepkit/injector`의 Dependency Injection Container에 의해 관리됩니다. Deepkit Framework를 사용할 때, 이들 Controller는 해당 Controller를 제공하는 모듈의 providers에 자동으로 접근할 수 있습니다. + +Deepkit Framework에서는 Controller가 Dependency Injection Scope `rpc`에서 인스턴스화되며, 이로 인해 모든 Controller는 이 스코프의 다양한 providers에 자동으로 접근할 수 있습니다. 추가로 제공되는 providers는 `HttpRequest`(옵션), `RpcInjectorContext`, `SessionState`, `RpcKernelConnection`, `ConnectionWriter`입니다. + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { App } from '@deepkit/app'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +new App({ + providers: [{ provide: Database, useValue: new Database }] + controllers: [Controller], +}).run(); +``` + +그러나 `RpcKernel`을 수동으로 인스턴스화할 때는 DI Container를 함께 전달할 수도 있습니다. 그러면 RPC Controller는 이 DI Container를 통해 인스턴스화됩니다. 이는 Express.js와 같은 Deepkit Framework가 아닌 환경에서 `@deepkit/rpc`를 사용하려는 경우에 유용합니다. + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { InjectorContext } from '@deepkit/injector'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +const injector = InjectorContext.forProviders([ + Controller, + { provide: Database, useValue: new Database }, +]); +const kernel = new RpcKernel(injector); +kernel.registerController(Controller); +``` + +자세한 내용은 [Dependency Injection](../dependency-injection.md)을 참조하세요. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/rpc/errors.md b/website/src/translations/ko/documentation/rpc/errors.md new file mode 100644 index 000000000..d096f925b --- /dev/null +++ b/website/src/translations/ko/documentation/rpc/errors.md @@ -0,0 +1,55 @@ +# 에러 + +던져진 에러는 에러 메시지와 stacktrace 같은 모든 정보와 함께 클라이언트로 자동 전달됩니다. + +에러 객체의 nominal 인스턴스가 중요하다면(예: instanceof를 사용하는 경우), runtime에 해당 error class가 등록되고 재사용되도록 `@entity.name('@error:unique-name')`를 사용해야 합니다. + +```typescript +@entity.name('@error:myError') +class MyError extends Error {} + +//server +@rpc.controller('/main') +class Controller { + @rpc.action() + saveUser(user: User): void { + throw new MyError('Can not save user'); + } +} + +//client +//[MyError]는 runtime에서 Class MyError가 인식되도록 보장합니다 +const controller = client.controller('/main', [MyError]); + +try { + await controller.getUser(2); +} catch (e) { + if (e instanceof MyError) { + //앗, 사용자를 저장할 수 없습니다 + } else { + //그 밖의 모든 에러 + } +} +``` + +## 에러 변환 + +던져진 에러는 에러 메시지와 stacktrace 등 모든 정보와 함께 클라이언트로 자동 전달되므로, 의도치 않게 민감한 정보가 공개될 수 있습니다. 이를 변경하기 위해 Method `transformError`에서 던져진 에러를 수정할 수 있습니다. + +```typescript +class MyKernelSecurity extends RpcKernelSecurity { + constructor(private logger: Logger) { + super(); + } + + transformError(error: Error) { + //새로운 Error로 감싸기 + this.logger.error('Error in RPC', error); + return new Error('Something went wrong: ' + error.message); + } +} +``` + +에러가 generic `Error`로 변환되면 전체 stack trace와 에러의 정체성(identity)이 사라집니다. 따라서 클라이언트에서 해당 에러에 대해 `instanceof` 체크를 사용할 수 없습니다. + +Deepkit RPC가 두 마이크로서비스 사이에서 사용되어 클라이언트와 서버가 개발자의 완전한 통제 하에 있는 경우, 에러를 변환할 필요는 거의 없습니다. 반면 클라이언트가 신뢰할 수 없는 브라우저 환경에서 실행되는 경우, 어떤 정보를 공개할지 `transformError`에서 주의 깊게 결정해야 합니다. 확신이 없다면 내부 세부사항이 유출되지 않도록 각 에러를 generic `Error`로 변환하는 것이 안전합니다. 이 시점에서 에러를 로깅하는 것도 좋은 아이디어입니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/rpc/getting-started.md b/website/src/translations/ko/documentation/rpc/getting-started.md new file mode 100644 index 000000000..30b1c9c1b --- /dev/null +++ b/website/src/translations/ko/documentation/rpc/getting-started.md @@ -0,0 +1,147 @@ +# 시작하기 + +Deepkit RPC를 사용하려면 Runtime Types에 기반하므로 `@deepkit/type`가 올바르게 설치되어 있어야 합니다. [Runtime Type 설치](../runtime-types.md)를 참고하세요. + +이 작업이 성공적으로 완료되면, 라이브러리를 내부적으로 이미 사용하는 Deepkit Framework 또는 `@deepkit/rpc`를 설치할 수 있습니다. + +```sh +npm install @deepkit/rpc +``` + +`@deepkit/rpc`의 Controller Class는 TypeScript decorator에 기반하며, 이 기능을 사용하려면 experimentalDecorators를 활성화해야 합니다. + +서버와 클라이언트가 각각의 package.json을 가지고 있다면 `@deepkit/rpc` 패키지는 서버와 클라이언트 모두에 설치해야 합니다. + +서버와 TCP로 통신하려면 클라이언트와 서버에 `@deepkit/rpc-tcp` 패키지를 설치해야 합니다. + +```sh +npm install @deepkit/rpc-tcp +``` + +WebSocket 통신의 경우에도 서버에는 이 패키지가 필요합니다. 반면 브라우저의 클라이언트는 표준 WebSocket을 사용합니다. + +클라이언트를 WebSocket이 없는 환경(예: NodeJS)에서도 사용하려면, 클라이언트에 ws 패키지가 필요합니다. + +```sh +npm install ws +``` + +## 사용법 + +아래는 WebSocket과 @deepkit/rpc의 low-level API를 기반으로 한 완전한 예제입니다. Deepkit Framework를 사용할 경우, Controller는 app module을 통해 제공되며 RpcKernel을 수동으로 인스턴스화하지 않습니다. + +_파일: server.ts_ + +```typescript +import { rpc, RpcKernel } from '@deepkit/rpc'; +import { RpcWebSocketServer } from '@deepkit/rpc-tcp'; + +@rpc.controller('/main') +export class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } +} + +const kernel = new RpcKernel(); +kernel.registerController(Controller); +const server = new RpcWebSocketServer(kernel, 'localhost:8081'); +server.start({ + host: '127.0.0.1', + port: 8081, +}); +console.log('Server started at ws://127.0.0.1:8081'); + +``` + +_파일: client.ts_ + +```typescript +import { RpcWebSocketClient } from '@deepkit/rpc'; +import type { Controller } from './server'; + +async function main() { + const client = new RpcWebSocketClient('ws://127.0.0.1:8081'); + const controller = client.controller('/main'); + + const result = await controller.hello('World'); + console.log('result', result); + + client.disconnect(); +} + +main().catch(console.error); + +``` + +## Server Controller + +Remote Procedure Call에서 "Procedure"라는 용어는 일반적으로 "Action"으로도 불립니다. Action은 클래스에 정의된 Method이며 `@rpc.action` decorator로 표시됩니다. 클래스 자체는 `@rpc.controller` decorator로 Controller로 표시되고 고유한 이름이 부여됩니다. 이 이름은 클라이언트에서 올바른 Controller를 지정할 때 참조됩니다. 필요한 만큼 여러 Controller를 정의하고 등록할 수 있습니다. + + +```typescript +import { rpc } from '@deepkit/rpc'; + +@rpc.controller('/main'); +class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } + + @rpc.action() + test(): boolean { + return true; + } +} +``` + +`@rpc.action()`으로 표시된 Method만 클라이언트에서 호출할 수 있습니다. + +Type은 명시적으로 지정해야 하며 추론될 수 없습니다. 이는 serializer가 네트워크로 전송되기 전에 Type의 형태를 정확히 알아야 이들을 바이너리 데이터(BSON) 또는 JSON으로 변환할 수 있기 때문입니다. + +## Client Controller + +일반적인 RPC 흐름에서는 클라이언트가 서버의 Function을 실행합니다. 하지만 Deepkit RPC에서는 서버가 클라이언트의 Function을 실행하는 것도 가능합니다. 이를 허용하려면 클라이언트도 Controller를 등록할 수 있습니다. + +TODO + +## 의존성 주입 (Dependency Injection) + +Deepkit Framework가 사용될 때, 클래스는 의존성 주입 컨테이너에 의해 인스턴스화되므로 애플리케이션의 다른 Provider에 자동으로 접근할 수 있습니다. + +또한 [의존성 주입 (Dependency Injection)](dependency-injection.md#)을 참고하세요. + +## RxJS 스트리밍 + +TODO + +## Nominal Types + +클라이언트가 Function 호출에서 데이터를 받을 때, 먼저 서버에서 직렬화되고 이후 클라이언트에서 역직렬화됩니다. Function의 Return Type에 Class가 포함되어 있으면, 해당 Class는 클라이언트 측에서 재구성되지만, nominal identity와 연결된 Method를 잃게 됩니다. 이 문제를 해결하려면 Class를 고유한 ID/이름으로 Nominal Type으로 등록하세요. 이 접근 방식은 RPC-API에서 사용되는 모든 Class에 적용해야 합니다. + +Class를 등록하려면 `@entity.name('id')` decorator를 사용합니다. + +```typescript +import { entity } from '@deepkit/type'; + +@entity.name('user') +class User { + id!: number; + firstName!: string; + lastName!: string; + get fullName() { + return this.firstName + ' ' + this.lastName; + } +} +``` + +이 Class가 Function의 결과로 사용되면, 그 identity가 보존됩니다. + +```typescript +const controller = client.controller('/main'); + +const user = await controller.getUser(2); +user instanceof User; //true when @entity.name is used, and false if not +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/rpc/security.md b/website/src/translations/ko/documentation/rpc/security.md new file mode 100644 index 000000000..cbb6d8d16 --- /dev/null +++ b/website/src/translations/ko/documentation/rpc/security.md @@ -0,0 +1,132 @@ +# 보안 + +기본적으로 모든 RPC Function은 어떤 클라이언트든 호출할 수 있으며, peer-to-peer 통신 기능이 활성화되어 있습니다. 어떤 클라이언트가 무엇을 할 수 있는지 세밀하게 제어하려면 `RpcKernelSecurity` Class를 override 할 수 있습니다. + +```typescript +import { RpcKernelSecurity, Session, RpcControllerAccess } from '@deepkit/type'; + +//기본 구현이 포함되어 있습니다 +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + return true; + } + + async isAllowedToRegisterAsPeer(session: Session, peerId: string): Promise { + return true; + } + + async isAllowedToSendToPeer(session: Session, peerId: string): Promise { + return true; + } + + async authenticate(token: any): Promise { + throw new Error('Authentication not implemented'); + } + + transformError(err: Error) { + return err; + } +} +``` + +이를 사용하려면 `RpcKernel`에 provider를 전달하세요: + +```typescript +const kernel = new RpcKernel([{provide: RpcKernelSecurity, useClass: MyKernelSecurity, scope: 'rpc'}]); +``` + +또는 Deepkit app의 경우, app에서 provider로 `RpcKernelSecurity` Class를 override 합니다: + +```typescript +import { App } from '@deepkit/type'; +import { RpcKernelSecurity } from '@deepkit/rpc'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + controllers: [MyRpcController], + providers: [ + {provide: RpcKernelSecurity, useClass: MyRpcKernelSecurity, scope: 'rpc'} + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 인증 / 세션 + +기본적으로 `Session` 객체는 익명 세션이며, 이는 클라이언트가 인증되지 않았음을 의미합니다. 클라이언트가 인증을 원할 때 `authenticate` Method가 호출됩니다. `authenticate` Method가 받는 token은 클라이언트로부터 오며 어떤 값이든 될 수 있습니다. + +클라이언트가 token을 설정하면, 첫 번째 RPC Function이 호출될 때 또는 `client.connect()`가 수동으로 호출될 때 인증이 수행됩니다. + + +```typescript +const client = new RpcWebSocketClient('localhost:8081'); +client.token.set('123456789'); + +const controller = client.controller('/main'); +``` + +이 경우 `RpcKernelSecurity.authenticate`는 token `123456789`를 받아 그에 따라 다른 세션을 반환할 수 있습니다. 반환된 세션은 `hasControllerAccess`와 같은 다른 모든 Method로 전달됩니다. + +```typescript +import { Session, RpcKernelSecurity } from '@deepkit/rpc'; + +class UserSession extends Session { +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.controllerClassType instanceof MySecureController) { + //MySecureController는 UserSession이 필요합니다 + return session instanceof UserSession; + } + return true; + } + + async authenticate(token: any): Promise { + if (token === '123456789') { + //username은 ID 또는 사용자명일 수 있습니다 + return new UserSession('username', token); + } + throw new Error('Authentication failed'); + } +} +``` + +## 컨트롤러 접근 + +`hasControllerAccess` Method는 클라이언트가 특정 RPC Function을 실행할 수 있는지를 결정합니다. 이 Method는 모든 RPC Function 호출마다 실행됩니다. `false`를 반환하면 접근이 거부되고, 클라이언트에서 Error가 발생합니다. + +`RpcControllerAccess`에는 해당 RPC Function에 대한 유용한 정보가 포함됩니다: + +```typescript +interface RpcControllerAccess { + controllerName: string; + controllerClassType: ClassType; + actionName: string; + actionGroups: string[]; + actionData: { [name: string]: any }; +} +``` + +Group과 추가 data는 decorator `@rpc.action()`을 통해 변경할 수 있습니다: + +```typescript +class Controller { + @rpc.action().group('secret').data('role', 'admin') + saveUser(user: User): void { + } +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.actionGroups.includes('secret')) { + if (session instanceof UserSession) { + //todo: 확인 + return session.username === 'admin'; + } + return false; + } + return true; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/rpc/transport.md b/website/src/translations/ko/documentation/rpc/transport.md new file mode 100644 index 000000000..dbecbd673 --- /dev/null +++ b/website/src/translations/ko/documentation/rpc/transport.md @@ -0,0 +1,17 @@ +# 전송 프로토콜 + +Deepkit RPC는 여러 전송 프로토콜을 지원합니다. WebSockets는 브라우저가 지원하기 때문에 호환성이 가장 좋으며, 스트리밍과 같은 모든 기능을 지원합니다. TCP는 일반적으로 더 빠르며 서버 간(마이크로서비스) 또는 비브라우저 클라이언트 간 통신에 적합합니다. 하지만 WebSockets도 서버 간 통신에 잘 동작합니다. + +## HTTP + +Deepkit의 RPC HTTP 프로토콜은 각 함수 호출이 HTTP 요청이기 때문에 브라우저에서 디버깅하기 특히 쉽지만, RxJS 스트리밍을 지원하지 않는 등의 제약이 있습니다. + +TODO: 아직 구현되지 않았습니다. + +## WebSockets + +@deepkit/rpc-tcp `RpcWebSocketServer` 및 브라우저 WebSocket 또는 Node `ws` 패키지. + +## TCP + +@deepkit/rpc-tcp `RpcNetTcpServer` 와 `RpcNetTcpClientAdapter` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types.md b/website/src/translations/ko/documentation/runtime-types.md new file mode 100644 index 000000000..8dba3e455 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types.md @@ -0,0 +1,9 @@ +# 런타임 타입 + +TypeScript의 런타임 타입 정보는 이전에는 불가능하거나 우회가 필요했던 새로운 워크플로와 기능을 가능하게 합니다. 현대 개발 프로세스는 GraphQL, validators, ORMs, ProtoBuf 같은 인코더 등의 도구를 위해 타입과 스키마 선언에 크게 의존합니다. 이러한 도구는 ProtoBuf와 GraphQL이 고유한 선언 언어를 갖거나, validators가 자체 schema API 또는 JSON-Schema를 사용하는 것처럼, 사용 사례에 특화된 새로운 언어를 학습하도록 개발자에게 요구할 수 있습니다. + +TypeScript는 복잡한 구조를 설명하고 심지어 GraphQL, ProtoBuf, JSON-Schema와 같은 선언 형식을 완전히 대체할 만큼 강력해졌습니다. 런타임 타입 시스템을 사용하면 코드 생성기나 "Zod" 같은 runtime JavaScript type declaration 라이브러리 없이도 이러한 도구의 사용 사례를 포괄할 수 있습니다. Deepkit 라이브러리는 런타임 타입 정보를 제공하고, 효율적이고 호환 가능한 솔루션을 더 쉽게 개발할 수 있도록 하는 것을 목표로 합니다. + +Deepkit은 가능한 한 많은 TypeScript 타입 정보를 활용하여 효율성을 높이고, 런타임에 타입 정보를 읽는 능력 위에 구축되었습니다. 런타임 타입 시스템은 Class Property, Function Parameter, Return Type과 같은 동적 타입을 읽고 계산할 수 있게 합니다. Deepkit은 TypeScript의 컴파일 과정에 훅을 걸어, [커스텀 바이트코드와 가상 머신](https://github.com/microsoft/TypeScript/issues/47658)을 사용해 모든 타입 정보가 생성된 JavaScript에 포함되도록 보장하여, 개발자가 프로그래밍적으로 타입 정보에 접근할 수 있게 합니다. + +Deepkit을 사용하면 개발자는 기존 TypeScript 타입을 런타임에서 validation, serialisation 등에 활용할 수 있어, 개발 과정을 단순화하고 효율성을 높일 수 있습니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/bytecode.md b/website/src/translations/ko/documentation/runtime-types/bytecode.md new file mode 100644 index 000000000..2727333cd --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/bytecode.md @@ -0,0 +1,76 @@ +# 바이트코드 + +JavaScript에서 Deepkit이 타입 정보를 어떻게 인코딩하고 읽는지 자세히 배우기 위한 장입니다. 타입이 실제로 어떻게 바이트코드로 변환되고, JavaScript로 내보내지며, 런타임에 해석되는지를 설명합니다. + +## Typen-Compiler + +type compiler(@deepkit/type-compiler)는 TypeScript 파일에 정의된 타입을 읽고 이를 바이트코드로 컴파일하는 역할을 합니다. 이 바이트코드는 런타임에서 타입을 실행하는 데 필요한 모든 것을 포함합니다. +작성 시점 기준으로, type compiler는 소위 TypeScript transformer입니다. 이 transformer는 TypeScript compiler 자체를 위한 plugin이며, TypeScript AST(Abstract Syntax Tree)를 다른 TypeScript AST로 변환합니다. Deepkit의 type compiler는 이 과정에서 AST를 읽고, 해당하는 바이트코드를 생성한 다음 그것을 AST에 삽입합니다. + +TypeScript 자체는 tsconfig.json을 통해 이 plugin(= transformer)을 구성할 수 있게 해주지 않습니다. TypeScript compiler API를 직접 사용하거나, Webpack과 ts-loader 같은 build system을 사용해야 합니다. Deepkit 사용자의 불편을 줄이기 위해, Deepkit type compiler는 @deepkit/type-compiler가 설치될 때마다 node_modules/typescript에 스스로를 자동으로 설치합니다. 이에 따라 로컬에 설치된 TypeScript(node_modules/typescript)를 사용하는 모든 build tool에서 type compiler가 자동으로 활성화됩니다. 이로써 tsc, Angular, webpack, ts-node 등 여러 도구가 Deepkit의 type compiler와 자동으로 함께 동작합니다. + +만약 NPM install script의 자동 실행이 비활성화되어 로컬에 설치된 TypeScript가 수정되지 않는 경우, 원한다면 이 과정을 수동으로 실행해야 합니다. 또는 webpack과 같은 build tool에서 type compiler를 수동으로 사용할 수도 있습니다. 위의 설치 섹션을 참조하세요. + +## 바이트코드 인코딩 + +바이트코드는 가상 머신을 위한 명령 시퀀스이며, JavaScript 내에서 참조와 문자열(실제 바이트코드)의 배열로 인코딩됩니다. + +```typescript +//TypeScript +type TypeA = string; + +//생성된 JavaScript +const typeA = ['&']; +``` + +기존 명령들은 각각 1바이트 크기이며, `@deepkit/type-spec`의 `ReflectionOp` enum에서 확인할 수 있습니다. 작성 시점 기준으로 명령 집합은 81개 이상의 명령으로 구성됩니다. + +```typescript +enum ReflectionOp { + never, + any, + unknown, + void, + object, + + string, + number, + + //...그 외 다수 +} +``` + +명령 시퀀스는 메모리를 절약하기 위해 문자열로 인코딩됩니다. 따라서 타입 `string[]`은 바이트코드 프로그램 `[string, array]`로 개념화되며, 바이트 `[5, 37]`을 갖고 다음 알고리즘으로 인코딩됩니다: + +```typescript +function encodeOps(ops: ReflectionOp[]): string { + return ops.map(v => String.fromCharCode(v + 33)).join(''); +} +``` + +이에 따라 5는 문자 '&'가 되고 37은 문자 'F'가 됩니다. 함께 하면 '&F'가 되며 JavaScript에서는 `['&F']`로 출력됩니다. + +```typescript +//TypeScript +export type TypeA = string[]; + +//생성된 JavaScript +export const __ΩtypeA = ['&F']; +``` + +이름 충돌을 방지하기 위해 각 타입에는 '_Ω' 접두사가 부여됩니다. export되었거나 export된 타입에서 사용되는 명시적으로 정의된 각 타입에 대해 JavaScript로 바이트코드가 내보내집니다. Class와 Function 또한 속성으로 직접 바이트코드를 가집니다. + +```typescript +//TypeScript +function log(message: string): void {} + +//생성된 JavaScript +function log(message) {} +log.__type = ['message', 'log', 'P&2!$/"']; +``` + +## 가상 머신 + +런타임에는 가상 머신(`@deepkit/type`의 class Processor)이 인코딩된 바이트코드를 디코딩하고 실행하는 책임을 집니다. 이는 [리플렉션 API](./reflection.md)에 설명된 대로 항상 type object를 반환합니다. + +더 많은 정보는 [TypeScript 바이트코드 인터프리터 / 런타임 타입 #47658](https://github.com/microsoft/TypeScript/issues/47658)에서 확인할 수 있습니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/custom-serializer.md b/website/src/translations/ko/documentation/runtime-types/custom-serializer.md new file mode 100644 index 000000000..f496f4b81 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/custom-serializer.md @@ -0,0 +1,99 @@ +# Custom Serializer + +기본적으로 `@deepkit/type`는 JSON serializer와 TypeScript 타입을 위한 type validation을 제공합니다. 이를 확장하여 serialization 기능을 추가/제거하거나 validation 방식(validator가 serializer와도 연결되어 있음)을 변경할 수 있습니다. + +## New Serializer + +serializer는 등록된 serializer templates와 함께 `Serializer` Class의 인스턴스일 뿐입니다. serializer template은 JIT serializer process를 위해 JavaScript 코드를 생성하는 작은 Function입니다. 각 Type(String, Number, Boolean 등)마다 데이터 변환 또는 validation을 위한 코드를 반환하는 별도의 serializer template이 있습니다. 이 코드는 사용자가 사용하는 JavaScript 엔진과 호환되어야 합니다. + +컴파일러 template Function이 실행되는 동안에만(또는 그렇게 되어야만) 전체 Type에 완전히 접근할 수 있습니다. 아이디어는 타입 변환에 필요한 모든 정보를 JavaScript 코드에 직접 포함시켜 매우 최적화된 코드(JIT-optimized code라고도 함)를 생성하는 것입니다. + +다음 예시는 빈 serializer를 생성합니다. + +```typescript +import { EmptySerializer } from '@deepkit/type'; + +class User { + name: string = ''; + created: Date = new Date; +} + +const mySerializer = new EmptySerializer('mySerializer'); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 0 } +``` + +보시다시피 아무 것도 변환되지 않았습니다(`created`는 여전히 number이지만, 우리는 이를 `Date`로 정의했습니다). 이를 변경하기 위해, Date Type의 deserialization을 위한 serializer template을 추가합니다. + +```typescript +mySerializer.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); +}); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 2021-06-10T19:34:27.301Z } +``` + +이제 우리의 serializer는 값을 Date Object로 변환합니다. + +serialization에도 동일하게 하려면, 또 다른 serialization template을 등록합니다. + +```typescript +mySerializer.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); +}); + +const user1 = new User(); +user1.name = 'Peter'; +user1.created = new Date('2021-06-10T19:34:27.301Z'); +console.log(serialize(user1, undefined, mySerializer)); +``` + +```sh +{ name: 'Peter', created: '2021-06-10T19:34:27.301Z' } +``` + +새 serializer는 이제 serialization 과정에서 Date Object의 날짜를 문자열로 올바르게 변환합니다. + +## Examples + +더 많은 예시를 보려면 Deepkit Type에 포함된 [JSON Serializer들](https://github.com/deepkit/deepkit-framework/blob/master/packages/type/src/serializer.ts#L1688)의 코드를 살펴보세요. + +## Extending Existing Serializer + +기존 serializer를 확장하고 싶다면 class 상속을 사용하면 됩니다. 이는 serializer가 constructor에서 자신의 template을 등록하도록 작성되어야 하기 때문에 가능합니다. + +```typescript +class MySerializer extends Serializer { + constructor(name: string = 'mySerializer') { + super(name); + this.registerTemplates(); + } + + protected registerTemplates() { + this.deserializeRegistry.register(ReflectionKind.string, (type, state) => { + state.addSetter(`String(${state.accessor})`); + }); + + this.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); + }); + + this.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); + }); + } +} +const mySerializer = new MySerializer(); +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/extend.md b/website/src/translations/ko/documentation/runtime-types/extend.md new file mode 100644 index 000000000..baf9091f7 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/extend.md @@ -0,0 +1,56 @@ +# 확장 + +## 사용자 정의 Serialization + +타입의 serialization을 확장하려면 직접 `Serializer`를 작성하거나 기본 `serializer`를 확장할 수 있습니다. + +이 예제는 `Point` class를 튜플 `[number, number]`로 serialize/deserialize하는 방법을 보여줍니다. + +```typescript +import { serializer, SerializationError } from '@deepkit/type'; + +class Point { + constructor(public x: number, public y: number) { + } +} + +// deserialize는 JSON에서 (class) instance로의 변환을 의미합니다. +serializer.deserializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: any) => { + // 이미 Point 인스턴스라면 완료입니다 + if (v instanceof Point) return v; + + // 이 시점에서 `v`는 (undefined를 제외하고) 어떤 값도 될 수 있으므로 검사가 필요합니다 + if (!Array.isArray(v)) throw new SerializationError('Expected array'); + if (v.length !== 2) throw new SerializationError('Expected array with two elements'); + if (typeof v[0] !== 'number' || typeof v[1] !== 'number') throw new SerializationError('Expected array with two numbers'); + return new Point(v[0], v[1]); + }); +}); + +serializer.serializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: Point) => { + // 이 시점에서 `v`는 항상 Point 인스턴스입니다 + return [v.x, v.y]; + }); +}); + +// cast와 deserialize는 기본적으로 `serializer`를 사용합니다 +const point = cast([1, 2], undefined, serializer); +expect(point).toBeInstanceOf(Point); +expect(point.x).toBe(1); +expect(point.y).toBe(2); + +{ + expect(() => deserialize(['vbb'])).toThrowError(SerializationError); + expect(() => deserialize(['vbb'])).toThrow('Expected array with two elements') +} + +// serialize는 기본적으로 `serializer`를 사용합니다 +const json = serialize(point); +expect(json).toEqual([1, 2]); +``` + +이는 `cast`, `deserialize`, `serialize`와 같은 일반 `@deepkit/type` Function에만 동작합니다. + +이는 database layer로 전달되지 않습니다. database layer는 migration과 serialization을 위해 Entity class에 정의된 type을 사용하기 때문입니다(예: BSON serialization). \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/external-types.md b/website/src/translations/ko/documentation/runtime-types/external-types.md new file mode 100644 index 000000000..f6f38cb63 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/external-types.md @@ -0,0 +1,49 @@ +# 외부 Type + +## 외부 Class + +TypeScript는 기본적으로 Type 정보를 포함하지 않으므로, 다른 패키지에서 import된 Type/Class(@deepkit/type-compiler를 사용하지 않은 경우)는 런타임에 사용할 수 있는 Type 정보가 제공되지 않습니다. + +외부 Class에 대한 Type을 annotate하려면 `annotateClass`를 사용하고, import된 Class가 다른 곳에서 사용되기 전에 이 Function이 애플리케이션의 bootstrap 단계에서 실행되도록 하세요. + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +interface AnnotatedClass { + id: number; + title: string; +} + +annotateClass(MyExternalClass); + +//이제 MyExternalClass의 모든 사용은 AnnotatedClass의 Type을 반환합니다 +serialize({...}); + +//이제 MyExternalClass는 다른 Type에서도 사용할 수 있습니다 +interface User { + id: number; + clazz: MyExternalClass; +} +``` + +이제 `MyExternalClass`는 serialization Function과 reflection API에서 사용할 수 있습니다. + +다음은 generic Class를 annotate하는 방법을 보여줍니다: + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +class AnnotatedClass { + id!: T; +} + +annotateClass(ExternalClass, AnnotatedClass); +``` + +## Import Type + +`import type` 구문은 실제 JavaScript 코드를 import하지 않고 type-checking에만 사용하도록 TypeScript에서 설계되었습니다. 이는 예를 들어 런타임에는 존재하지 않고 컴파일 타임에만 존재하는 패키지의 Type을 사용하고 싶거나, 런타임에 해당 패키지를 실제로 로드하고 싶지 않을 때 유용합니다. + +Deepkit은 `import type`의 취지를 따르며 어떤 런타임 코드도 생성하지 않습니다. 즉, `import type`을 사용하면 런타임에는 어떠한 Type 정보도 제공되지 않습니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/getting-started.md b/website/src/translations/ko/documentation/runtime-types/getting-started.md new file mode 100644 index 000000000..7aca86854 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/getting-started.md @@ -0,0 +1,106 @@ +# 시작하기 + +Deepkit의 runtime type system을 설치하려면 두 패키지가 필요합니다: Deepkit Type Compiler와 Deepkit Type 패키지 자체. Type compiler는 TypeScript transformer로서 TypeScript types로부터 runtime type information을 생성합니다. type 패키지에는 runtime virtual machine과 type annotations, 그리고 types를 다루기 위한 많은 유용한 함수들이 포함되어 있습니다. + + +## 설치 + +```sh +npm install --save @deepkit/type +npm install --save-dev @deepkit/type-compiler typescript ts-node +``` + +기본적으로 runtime type information은 생성되지 않습니다. 이를 활성화하려면 `tsconfig.json` 파일에 `"reflection": true`를 설정해야 합니다. + +decorators를 사용하려면 `tsconfig.json`에서 `"experimentalDecorators": true`를 활성화해야 합니다. 이는 `@deepkit/type`을 사용하는 데 엄격히 필요하지는 않지만, 다른 Deepkit 라이브러리의 일부 기능 및 Deepkit Framework에서는 필요합니다. + +_파일: tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +runtime type information을 사용하는 첫 코드를 작성하세요: + +_파일: app.ts_ + +```typescript +import { cast, MinLength, ReflectionClass } from '@deepkit/type'; + +interface User { + username: string & MinLength<3>; + birthDate?: Date; +} + +const user = cast({ + username: 'Peter', + birthDate: '2010-10-10T00:00:00Z' +}); +console.log(user); + +const reflection = ReflectionClass.from(); +console.log(reflection.getProperty('username').type); +``` + +그리고 `ts-node`로 실행하세요: + +```sh +./node_modules/.bin/ts-node app.ts +``` + +## 대화형 예제 + +다음은 codesandbox 예제입니다: https://codesandbox.io/p/sandbox/deepkit-runtime-types-fjmc2f?file=index.ts + +## Type compiler + +TypeScript 자체는 type compiler를 `tsconfig.json`으로 구성할 수 있도록 지원하지 않습니다. TypeScript compiler API를 직접 사용하거나, Webpack과 같은 build system에서 _ts-loader_를 사용해야 합니다. Deepkit 사용자들이 이러한 번거로움을 겪지 않도록, `@deepkit/type-compiler`가 설치되면 Deepkit type compiler는 자동으로 `node_modules/typescript`에 자신을 설치합니다(NPM install hooks를 통해 수행됩니다). +이에 따라 로컬에 설치된 TypeScript(`node_modules/typescript`에 있는 것)를 참조하는 모든 build tool에서 type compiler가 자동으로 활성화됩니다. 그 결과 _tsc_, Angular, webpack, _ts-node_ 및 일부 다른 도구들이 deepkit type compiler와 자동으로 함께 작동합니다. + +type compiler가 자동으로 성공적으로 설치되지 않은 경우(예: NPM install hooks가 비활성화되어 있는 경우), 다음 명령으로 수동으로 설치할 수 있습니다: + +```sh +node_modules/.bin/deepkit-type-install +``` + +로컬 typescript 버전이 업데이트된 경우(예: package.json의 typescript 버전이 변경되고 `npm install`을 실행한 경우), `deepkit-type-install`을 실행해야 한다는 점에 유의하세요. + +## Webpack + +webpack 빌드에서 type compiler를 사용하려면 `ts-loader` 패키지(또는 transformer 등록을 지원하는 다른 typescript loader)를 사용할 수 있습니다. + +_파일: webpack.config.js_ + +```javascript +const typeCompiler = require('@deepkit/type-compiler'); + +module.exports = { + entry: './app.ts', + module: { + rules: [ + { + test: /\.tsx?$/, + use: { + loader: 'ts-loader', + options: { + //이는 @deepkit/type의 type compiler를 활성화합니다 + getCustomTransformers: (program, getProgram) => ({ + before: [typeCompiler.transformer], + afterDeclarations: [typeCompiler.declarationTransformer], + }), + } + }, + exclude: /node_modules/, + }, + ], + }, +} +``` \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/reflection.md b/website/src/translations/ko/documentation/runtime-types/reflection.md new file mode 100644 index 000000000..7aaa77bfd --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/reflection.md @@ -0,0 +1,291 @@ +# 리플렉션 + +Type 정보 자체를 직접 다루기 위해서는 두 가지 기본 방식이 있습니다: Type objects와 Reflection classes. Type objects는 `typeOf()`가 반환하는 일반 JS 객체입니다. Reflection classes는 아래에서 설명합니다. + + +`typeOf` Function은 Interface, object literal, class, function, type alias를 포함한 모든 타입에서 작동합니다. 이 Function은 타입에 대한 모든 정보를 담고 있는 type object를 반환합니다. generics를 포함해 어떤 타입이든 type argument로 전달할 수 있습니다. + +```typescript +import { typeOf } from '@deepkit/type'; + +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} + +typeOf<{id: number}>(); //{kind: 4, types: [{kind: 6, name: 'id'}]} + +class User { + id: number +} + +typeOf(); //{kind: 4, types: [...]} + +function test(id: number): string {} + +typeOf(); //{kind: 12, parameters: [...], return: {kind: 5}} +``` + +type object는 해당 type object의 타입을 나타내는 `kind` Property를 포함하는 단순한 object literal입니다. `kind` Property는 숫자이며 `ReflectionKind` enum에서 그 의미를 얻습니다. `ReflectionKind`는 `@deepkit/type` 패키지에 다음과 같이 정의되어 있습니다: + +```typescript +enum ReflectionKind { + never, //0 + any, //1 + unknown, //2 + void, //3 + object, //4 + string, //5 + number, //6 + boolean, //7 + symbol, //8 + bigint, //9 + null, //10 + undefined, //11 + + //... and even more +} +``` + +반환될 수 있는 type object는 여러 가지가 있습니다. 가장 단순한 것들은 `never`, `any`, `unknown`, `void, null,` 그리고 `undefined`이며, 다음과 같이 표현됩니다: + +```typescript +{kind: 0}; //never +{kind: 1}; //any +{kind: 2}; //unknown +{kind: 3}; //void +{kind: 10}; //null +{kind: 11}; //undefined +``` + +예를 들어 숫자 0은 `ReflectionKind` enum의 첫 번째 항목(이 경우 `never`)이고, 숫자 1은 두 번째 항목(여기서는 `any`)이며, 이런 식입니다. 이에 따라 `string`, `number`, `boolean`과 같은 primitive type은 다음과 같이 표현됩니다: + +```typescript +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} +typeOf(); //{kind: 7} +``` + +이러한 비교적 단순한 타입은 `typeOf`에 직접 type argument로 전달되었기 때문에 type object에 추가 정보가 없습니다. 그러나 type alias를 통해 타입이 전달된 경우에는 type object에 추가 정보를 찾을 수 있습니다. + +```typescript +type Title = string; + +typeOf(); //{kind: 5, typeName: 'Title'} +``` + +이 경우 type alias의 이름 'Title'도 사용할 수 있습니다. type alias가 generic이면, 전달된 타입들도 type object에서 확인할 수 있습니다. + +```typescript +type Title<T> = T extends true ? string : number; + +typeOf<Title<true>>(); +{kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]} +``` + +전달된 타입이 index access operator의 결과인 경우, container와 index 타입이 함께 존재합니다: + +```typescript +interface User { + id: number; + username: string; +} + +typeOf<User['username']>(); +{kind: 5, indexAccessOrigin: { + container: {kind: Reflection.objectLiteral, types: [...]}, + Index: {kind: Reflection.literal, literal: 'username'} +}} +``` + +Interface와 object literal은 모두 Reflection.objectLiteral로 출력되며, `types` 배열에 Property와 Method를 포함합니다. + +```typescript +interface User { + id: number; + username: string; + login(password: string): void; +} + +typeOf<User>(); +{ + kind: Reflection.objectLiteral, + types: [ + {kind: Reflection.propertySignature, name: 'id', type: {kind: 6}}, + {kind: Reflection.propertySignature, name: 'username', + type: {kind: 5}}, + {kind: Reflection.methodSignature, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} + +type User = { + id: number; + username: string; + login(password: string): void; +} +typeOf<User>(); //위와 동일한 객체를 반환 +``` + +index signature도 `types` 배열에 있습니다. + +```typescript +interface BagOfNumbers { + [name: string]: number; +} + + +typeOf<BagOfNumbers>; +{ + kind: Reflection.objectLiteral, + types: [ + { + kind: Reflection.indexSignature, + index: {kind: 5}, //string + type: {kind: 6}, //number + } + ] +} + +type BagOfNumbers = { + [name: string]: number; +} +typeOf<BagOfNumbers>(); //위와 동일한 객체를 반환 +``` + +class는 object literal과 유사하며, `classType`(class 자체에 대한 reference)와 함께 `types` 배열 아래에 Property와 Method를 가집니다. + +```typescript +class User { + id: number = 0; + username: string = ''; + login(password: string): void { + //아무 것도 하지 않음 + } +} + +typeOf<User>(); +{ + kind: Reflection.class, + classType: User, + types: [ + {kind: Reflection.property, name: 'id', type: {kind: 6}}, + {kind: Reflection.property, name: 'username', + type: {kind: 5}}, + {kind: Reflection.method, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} +``` + +Reflection.propertySignature의 타입이 Reflection.property로, Reflection.methodSignature가 Reflection.method로 변경되었음을 주의하세요. class의 Property와 Method에는 추가 속성들이 있으므로 이 정보도 조회할 수 있습니다. 후자에는 `visibility`, `abstract`, `default`가 추가로 포함됩니다. +class의 type object에는 super-class의 Property와 Method가 아니라 해당 class 자신만의 Property와 Method만 포함됩니다. 이는 Interface/object literal의 type object가 모든 부모의 property signature와 method signature를 `types`에 해석해 포함하는 것과는 반대입니다. super-class의 Property와 Method를 해석하려면 ReflectionClass와 그 `ReflectionClass.getProperties()`(다음 섹션 참조) 또는 `@deepkit/type`의 `resolveTypeMembers()`를 사용할 수 있습니다. + +type object는 정말 다양합니다. 예를 들어 literal, template literal, promise, enum, union, array, tuple 등 많은 것들이 있습니다. 어떤 것이 존재하며 어떤 정보가 제공되는지 알아보려면 `@deepkit/type`에서 `Type`을 import하는 것을 권장합니다. 이것은 TypeAny, TypeUnknonwn, TypeVoid, TypeString, TypeNumber, TypeObjectLiteral, TypeArray, TypeClass 등 모든 가능한 subtype을 포함한 `union`입니다. 거기에서 정확한 구조를 확인할 수 있습니다. + +## Type 캐시 + +type alias, function, class에 대해 generic argument가 전달되지 않는 한 type object는 캐시됩니다. 이는 `typeOf<MyClass>()` 호출이 항상 동일한 객체를 반환한다는 의미입니다. + +```typescript +type MyType = string; + +typeOf<MyType>() === typeOf<MyType>(); //true +``` + +그러나 generic type이 사용되는 순간, 전달된 타입이 항상 동일하더라도 새로운 객체가 항상 생성됩니다. 이는 이론적으로 무한한 조합이 가능하며, 그런 캐시는 사실상 메모리 누수가 되기 때문입니다. + +```typescript +type MyType<T> = T; + +typeOf<MyType<string>>() === typeOf<MyType<string>>(); +//false +``` + +다만, 재귀 타입에서 동일한 타입이 여러 번 인스턴스화되는 순간에는 캐시됩니다. 그러나 캐시의 지속 시간은 타입이 계산되는 순간으로만 제한되며 그 이후에는 존재하지 않습니다. 또한 Type object가 캐시되더라도 새로운 reference가 반환되며 동일한 객체 그 자체는 아닙니다. + +```typescript +type MyType<T> = T; +type Object = { + a: MyType<string>; + b: MyType<string>; +}; + +typeOf<Object>(); +``` + +`MyType<string>`는 `Object`가 계산되는 동안에만 캐시됩니다. 따라서 `a`와 `b`의 PropertySignature는 캐시에서 동일한 `type`을 가지지만, 동일한 Type object는 아닙니다. + +모든 non-root Type object는 일반적으로 둘러싼 상위 객체를 가리키는 parent Property를 가집니다. 이는 예를 들어 어떤 Type이 union의 일부인지 아닌지 확인하는 데 유용합니다. + +```typescript +type ID = string | number; + +typeOf<ID>(); +*Ref 1* { + kind: ReflectionKind.union, + types: [ + {kind: ReflectionKind.string, parent: *Ref 1* } } + {kind: ReflectionKind.number, parent: *Ref 1* } + ] +} +``` + +'Ref 1'은 실제 union type object를 가리킵니다. + +위에서 예로 든 캐시된 Type object의 경우, `parent` Property가 항상 실제 부모를 가리키지는 않습니다. 예를 들어 동일한 class가 여러 번 사용되는 경우, 비록 `types`(TypePropertySignature와 TypeMethodSignature)의 즉각적인 타입은 올바른 TypeClass를 가리키지만, 이 signature 타입들의 `type`은 캐시된 항목의 TypeClass의 signature 타입들을 가리킵니다. 이는 부모 구조를 무한히 읽지 말고 즉각적인 부모만 읽도록 하기 위해 알아두어야 합니다. parent가 무한 정밀도를 가지지 않는 것은 성능상의 이유입니다. + +## JIT 캐시 + +다음에 설명되는 일부 Function과 기능은 종종 type object를 기반으로 합니다. 이 중 일부를 성능 좋게 구현하기 위해서는 type object별 JIT(just in time) 캐시가 필요합니다. 이는 `getJitContainer(type)`를 통해 제공할 수 있습니다. 이 Function은 임의의 데이터를 저장할 수 있는 단순한 객체를 반환합니다. 그 객체에 대한 reference를 유지하지 않는 한, Type object 자체가 더 이상 reference되지 않을 때 GC에 의해 자동으로 삭제됩니다. + + +## Reflection Classes + +`typeOf<>()` Function 외에도 Type objects에 대한 OOP 대안을 제공하는 다양한 reflection classes가 있습니다. 이 reflection classes는 class, Interface/object literal, function 및 그들의 직접적인 하위 타입들(Properties, Methods, Parameters)에 대해서만 사용할 수 있습니다. 더 깊은 타입은 다시 Type objects로 읽어야 합니다. + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + + +const reflection = ReflectionClass.from<User>(); + +reflection.getProperties(); //[ReflectionProperty, ReflectionProperty] +reflection.getProperty('id'); //ReflectionProperty + +reflection.getProperty('id').name; //'id' +reflection.getProperty('id').type; //{kind: ReflectionKind.number} +reflection.getProperty('id').isOptional(); //false +``` + + +## 타입 정보 받기 + +타입을 대상으로 동작하는 Function을 제공하기 위해, 사용자에게 타입을 수동으로 전달할 수 있도록 하는 것이 유용할 수 있습니다. 예를 들어 validation Function에서는 첫 번째 type argument로 요청할 타입을, 첫 번째 function argument로 검증할 데이터를 제공하는 것이 유용할 수 있습니다. + +```typescript +validate<string>(1234); +``` + +이 Function이 `string` 타입을 얻기 위해서는, type compiler에 이를 알려야 합니다. + +```typescript +function validate<T>(data: any, type?: ReceiveType<T>): void; +``` + +첫 번째 type argument `T`에 대한 reference를 가진 `ReceiveType`은 type compiler에게 각 `validate` 호출 시 해당 타입을 두 번째 위치에 넣어야 함을 신호합니다(왜냐하면 `type`이 두 번째로 선언되어 있기 때문입니다). 그런 다음 런타임에 정보를 읽어내기 위해 `resolveReceiveType` Function을 사용합니다. + +```typescript +import { resolveReceiveType, ReceiveType } from '@deepkit/type'; + +function validate<T>(data: any, type?: ReceiveType<T>): void { + type = resolveReceiveType(type); +} +``` + +불필요하게 새 변수를 만들지 않도록 결과를 동일한 변수에 할당하는 것이 좋습니다. 이제 `type`에는 type object가 저장되거나, 예를 들어 type argument가 전달되지 않았거나, Deepkit의 type compiler가 올바르게 설치되지 않았거나, 타입 정보 방출이 활성화되지 않은 경우(위의 Installation 섹션 참조)에는 에러가 발생합니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/serialization.md b/website/src/translations/ko/documentation/runtime-types/serialization.md new file mode 100644 index 000000000..1ddea6ce1 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/serialization.md @@ -0,0 +1,153 @@ +# 직렬화 + +Serialization은 예를 들어 데이터 타입을 전송 또는 저장에 적합한 형식으로 변환하는 과정입니다. Deserialization은 이를 되돌리는 과정입니다. 이 과정은 무손실로 수행되어, 데이터 타입 정보나 데이터 자체를 잃지 않고 직렬화 대상과 상호 변환할 수 있습니다. + +JavaScript에서 직렬화는 보통 JavaScript 객체와 JSON 사이에서 이루어집니다. JSON은 String, Number, Boolean, Object, Array만을 지원합니다. 반면 JavaScript는 BigInt, ArrayBuffer, typed arrays, Date, 사용자 정의 class 인스턴스 등 훨씬 많은 타입을 지원합니다. JavaScript 데이터를 JSON으로 서버에 전송하려면 클라이언트 측의 직렬화 과정과 서버 측의 역직렬화 과정이 필요하며, 반대로 서버가 JSON으로 데이터를 클라이언트에 보낼 때도 마찬가지입니다. 이때 `JSON.parse`와 `JSON.stringify`만으로는 무손실이 아니기 때문에 충분하지 않은 경우가 많습니다. + +이 직렬화 과정은 비사소한 데이터에 필수적입니다. JSON은 심지어 date 같은 기본 타입의 정보도 잃어버리기 때문입니다. 새 Date는 결국 JSON에서 문자열로 직렬화됩니다: + +```typescript +const json = JSON.stringify(new Date); +//'"2022-05-13T20:48:51.025Z" +``` + +보시다시피 JSON.stringify의 결과는 JSON 문자열입니다. 이를 다시 JSON.parse로 역직렬화하면 date 객체가 아니라 문자열을 얻게 됩니다. + +```typescript +const value = JSON.parse('"2022-05-13T20:48:51.025Z"'); +//"2022-05-13T20:48:51.025Z" +``` + +JSON.parse에 Date 객체의 역직렬화를 가르치는 다양한 우회 방법이 있지만, 오류가 발생하기 쉽고 성능이 좋지 않습니다. 이 경우 및 많은 다른 타입에 대해 타입 안전한 직렬화와 역직렬화를 가능하게 하려면 별도의 직렬화 과정이 필요합니다. + +네 가지 주요 Function이 제공됩니다: `serialize`, `cast`, `deserialize`, `validatedDeserialize`. 이러한 Function의 내부에서는 기본적으로 `@deepkit/type`의 전역 JSON serializer가 사용되지만, 사용자 정의 직렬화 대상도 사용할 수 있습니다. + +Deepkit Type은 사용자 정의 직렬화 대상을 지원하며, 이미 강력한 JSON 직렬화 대상을 제공하여 데이터를 JSON 객체로 직렬화하고 `JSON.stringify`를 사용해 JSON으로 올바르고 안전하게 변환할 수 있습니다. `@deepkit/bson`을 사용하면 BSON도 직렬화 대상으로 사용할 수 있습니다. 사용자 정의 직렬화 대상(예: 데이터베이스 드라이버용)을 만드는 방법은 Custom Serializer 섹션에서 배울 수 있습니다. + +serializer도 호환성 검증을 수행하지만, 이러한 검증은 [검증](validation.md)의 검증과는 다릅니다. 오직 `cast` Function만이 역직렬화가 성공한 후 [검증](validation.md) 챕터의 전체 검증 프로세스를 호출하며, 데이터가 유효하지 않으면 Error를 던집니다. + +대안으로, 역직렬화 후 검증을 수행하기 위해 `validatedDeserialize`를 사용할 수 있습니다. 또 다른 대안으로는 `deserialize` Function으로 역직렬화한 데이터에 `validate` 또는 `validates` Function을 수동으로 호출하는 방법이 있습니다. 자세한 내용은 [검증](validation.md)을 참고하세요. + +직렬화와 검증의 모든 Function은 오류 시 `@deepkit/type`의 `ValidationError`를 던집니다. + +## Cast + +`cast` Function은 첫 타입 인수로 TypeScript 타입을, 두 번째 인수로 변환할 데이터를 받습니다. 데이터는 주어진 타입으로 캐스팅되며, 성공 시 그 데이터가 반환됩니다. 데이터가 주어진 타입과 호환되지 않고 자동 변환할 수 없으면 `ValidationError`가 던져집니다. + +```typescript +import { cast } from '@deepkit/type'; + +cast<string>(123); //'123' +cast<number>('123'); //123 +cast<number>('asdasd'); // throws ValidationError + +cast<string | number>(123); //123 +``` + +```typescript +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = cast<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); +``` + +`deserialize` Function은 `cast`와 유사하지만, 데이터가 주어진 타입과 호환되지 않을 때 Error를 던지지 않습니다. 대신 가능한 한 변환을 시도하고 그 결과를 반환합니다. 데이터가 주어진 타입과 호환되지 않으면 원본 데이터를 그대로 반환합니다. + +## 직렬화 + +```typescript +import { serialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const model = new MyModel('Peter'); + +const jsonObject = serialize<MyModel>(model); +//{ +// id: 0, +// created: '2021-06-10T15:07:24.292Z', +// name: 'Peter' +//} +const json = JSON.stringify(jsonObject); +``` + +`serialize` Function은 기본적으로 JSON serializer를 사용해 전달된 데이터를 JSON 객체, 즉 String, Number, Boolean, Object, 또는 Array로 변환합니다. 이 결과는 `JSON.stringify`를 사용해 안전하게 JSON으로 변환할 수 있습니다. + +## 역직렬화 + +`deserialize` Function은 기본적으로 JSON serializer를 사용해 전달된 데이터를 지정된 타입으로 변환합니다. JSON serializer는 JSON 객체, 즉 string, number, boolean, object, 또는 array를 입력으로 기대합니다. 이는 보통 `JSON.parse` 호출에서 얻습니다. + +```typescript +import { deserialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = deserialize<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); + +//from JSON +const json = '{"id": 5, "created": "Sat Oct 13 2018 14:17:35 GMT+0200", "name": "Peter"}'; +const myModel = deserialize<MyModel>(JSON.parse(json)); +``` + +이미 올바른 데이터 타입이 전달된 경우(예: `created`에 Date 객체)에는 그대로 사용됩니다. + +class뿐 아니라 어떤 TypeScript 타입이든 첫 타입 인수로 지정할 수 있습니다. 따라서 원시 타입이나 매우 복잡한 타입도 전달할 수 있습니다: + +```typescript +deserialize<Date>('Sat Oct 13 2018 14:17:35 GMT+0200'); +deserialize<string | number>(23); +``` + +<a name="loosely-convertion"></a> +### 느슨한 타입 변환 + +역직렬화 과정에는 느슨한 타입 변환이 구현되어 있습니다. 이는 String 타입에 대해 String과 Number를, 혹은 String 타입을 위해 Number를 받아 자동으로 변환할 수 있음을 의미합니다. 이는 예를 들어 URL을 통해 데이터를 받고 역직렬화기에 전달할 때 유용합니다. URL은 항상 문자열이므로, Deepkit Type은 Number와 Boolean 타입을 여전히 해석하려고 시도합니다. + +```typescript +deserialize<boolean>('false')); //false +deserialize<boolean>('0')); //false +deserialize<boolean>('1')); //true + +deserialize<number>('1')); //1 + +deserialize<string>(1)); //'1' +``` + +다음과 같은 느슨한 타입 변환이 JSON serializer에 내장되어 있습니다: + +* number|bigint: Number 또는 Bigint는 String, Number, BigInt를 허용합니다. 변환이 필요할 경우 `parseFloat` 또는 `BigInt(x)`가 사용됩니다. +* boolean: Boolean은 Number와 String을 허용합니다. 0, '0', 'false'는 `false`로 해석되고, 1, '1', 'true'는 `true`로 해석됩니다. +* string: String은 Number, String, Boolean 등 많은 값을 허용합니다. 문자열이 아닌 값은 모두 `String(x)`로 자동 변환됩니다. + +느슨한 변환은 비활성화할 수도 있습니다: + +```typescript +const result = deserialize(data, {loosely: false}); +``` + +유효하지 않은 데이터의 경우 변환을 시도하지 않고 대신 Error가 던져집니다. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/types.md b/website/src/translations/ko/documentation/runtime-types/types.md new file mode 100644 index 000000000..f3eed62e7 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/types.md @@ -0,0 +1,494 @@ +# 타입 애너테이션 + +Type annotation은 런타임에서 읽혀 다양한 Function의 동작을 변경할 수 있는 메타 정보를 포함하는 일반적인 TypeScript Type입니다. Deepkit은 이미 많은 사용 사례를 커버하는 여러 type annotation을 제공합니다. 예를 들어, class property를 primary key, reference, 또는 index로 표시할 수 있습니다. 데이터베이스 라이브러리는 이 정보를 런타임에 사용하여 사전 code 생성 없이 올바른 SQL 쿼리를 만들 수 있습니다. + +`MaxLength`, `Maximum`, `Positive`와 같은 Validator 제약을 어떤 type에도 추가할 수 있습니다. 특정 값을 serializer가 어떻게 serialize/deserialzie 할지 알려주는 것도 가능합니다. 또한 완전히 커스텀한 type annotation을 만들고 이를 런타임에 읽어, 런타임에서 type system을 매우 개별적으로 사용할 수도 있습니다. + +Deepkit은 `@deepkit/type`에서 바로 사용할 수 있는 다양한 type annotation을 제공합니다. 이들은 여러 라이브러리에서 오지 않도록 설계되어, Deepkit RPC나 Deepkit Database 같은 특정 라이브러리에 code를 직접 묶지 않게 합니다. 이는 예를 들어 데이터베이스 type annotation을 사용하더라도, frontend에서도 Type의 재사용을 더 쉽게 해줍니다. + +다음은 기존 type annotation의 목록입니다. `@deepkit/type`와 `@deepkit/bson`의 validator와 serializer, 그리고 `@deepkit/orm`의 Deepkit Database는 이 정보를 서로 다르게 사용합니다. 더 알아보려면 해당 장을 참고하세요. + +## Integer/Float + +정수와 실수는 기본적으로 `number`로 정의되며 여러 하위 변형이 있습니다: + +| Type | 설명 | +|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| integer | 임의 크기의 정수. | +| int8 | -128에서 127 사이의 정수. | +| uint8 | 0에서 255 사이의 정수. | +| int16 | -32768에서 32767 사이의 정수. | +| uint16 | 0에서 65535 사이의 정수. | +| int32 | -2147483648에서 2147483647 사이의 정수. | +| uint32 | 0에서 4294967295 사이의 정수. | +| float | number와 같지만 데이터베이스 문맥에서 다른 의미를 가질 수 있음. | +| float32 | -3.40282347e+38에서 3.40282347e+38 사이의 float. JavaScript는 정밀도 문제로 범위를 정확히 검사할 수 없지만, 이 정보는 데이터베이스나 바이너리 serializer에 유용할 수 있음. | +| float64 | number와 같지만 데이터베이스 문맥에서 다른 의미를 가질 수 있음. | + +```typescript +import { integer } from '@deepkit/type'; + +interface User { + id: integer; +} +``` + +여기서 사용자의 `id`는 런타임에서는 number이지만, validation과 serialization에서는 integer로 해석됩니다. +이는 예를 들어 validation에서는 float를 사용할 수 없고, serializer가 자동으로 float를 integer로 변환한다는 것을 의미합니다. + +```typescript +import { is, integer } from '@deepkit/type'; + +is<integer>(12); //true +is<integer>(12.5); //false +``` + +하위 타입들도 동일하게 사용할 수 있으며, 특정 숫자 범위만 허용해야 할 때 유용합니다. + +```typescript +import { is, int8 } from '@deepkit/type'; + +is<int8>(-5); //true +is<int8>(5); //true +is<int8>(-200); //false +is<int8>(2500); //false +``` + +```typescript +import { is, float, float32, float64 } from '@deepkit/type'; +is<float>(12.5); //true +is<float32>(12.5); //true +is<float64>(12.5); //true +```` + +## UUID + +UUID v4는 일반적으로 데이터베이스에서는 binary로, JSON에서는 string으로 저장됩니다. + +```typescript +import { is, UUID } from '@deepkit/type'; + +is<UUID>('f897399a-9f23-49ac-827d-c16f8e4810a0'); //true +is<UUID>('asd'); //false +``` + +## MongoID + +이 필드를 MongoDB의 ObjectId로 표시합니다. string으로 해석되며, MongoDB에는 binary로 저장됩니다. + +```typescript +import { MongoId, serialize, is } from '@deepkit/type'; + +serialize<MongoId>('507f1f77bcf86cd799439011'); //507f1f77bcf86cd799439011 +is<MongoId>('507f1f77bcf86cd799439011'); //true +is<MongoId>('507f1f77bcf86cd799439011'); //false + +class User { + id: MongoId = ''; //사용자가 삽입되면 Deepkit ORM에서 자동으로 설정됨 +} +``` + +## Bigint + +기본적으로 일반 bigint type은 JSON에서는 number로 (BSON에서는 long으로) serialize됩니다. 그러나 JavaScript의 bigint는 잠재적으로 크기가 무제한인 반면, JavaScript의 number와 BSON의 long은 제한이 있으므로 저장 가능한 범위에 제약이 생깁니다. 이 제한을 우회하기 위해 `BinaryBigInt`와 `SignedBinaryBigInt` type을 사용할 수 있습니다. + +`BinaryBigInt`는 bigint와 같지만, 데이터베이스에서는 무제한 크기의 unsigned binary(대부분의 데이터베이스의 8바이트 대신)로, JSON에서는 string으로 serialize됩니다. 음수 값은 양수로 변환됩니다(`abs(x)`). + +```typescript +import { BinaryBigInt } from '@deepkit/type'; + +interface User { + id: BinaryBigInt; +} + +const user: User = { id: 24n }; + +serialize<User>({ id: 24n }); //{id: '24'} + +serialize<BinaryBigInt>(24); //'24' +serialize<BinaryBigInt>(-24); //'0' +``` + +Deepkit ORM은 BinaryBigInt를 binary field로 저장합니다. + +`SignedBinaryBigInt`는 `BinaryBigInt`와 같지만 음수 값도 저장할 수 있습니다. Deepkit ORM은 `SignedBinaryBigInt`를 binary로 저장합니다. 이 binary에는 추가적인 선행 부호 byte가 있으며 uint로 표현됩니다: 음수는 255, 0은 0, 양수는 1. + +```typescript +import { SignedBinaryBigInt } from '@deepkit/type'; + +interface User { + id: SignedBinaryBigInt; +} +``` + +## MapName + +serialization에서 property의 이름을 변경합니다. + +```typescript +import { serialize, deserialize, MapName } from '@deepkit/type'; + +interface User { + firstName: string & MapName<'first_name'>; +} + +serialize<User>({ firstName: 'Peter' }) // {first_name: 'Peter'} +deserialize<User>({ first_name: 'Peter' }) // {firstName: 'Peter'} +``` + +## Group + +Property를 그룹으로 묶을 수 있습니다. 예를 들어 serialization 시 특정 그룹을 제외할 수 있습니다. 자세한 내용은 Serialization 장을 참고하세요. + +```typescript +import { serialize } from '@deepkit/type'; + +interface Model { + username: string; + password: string & Group<'secret'> +} + +serialize<Model>( + { username: 'Peter', password: 'nope' }, + { groupsExclude: ['secret'] } +); //{username: 'Peter'} +``` + +## Data + +각 property는 Reflection API를 통해 읽을 수 있는 추가 meta-data를 가질 수 있습니다. 자세한 내용은 [런타임 타입 리플렉션](runtime-types.md#runtime-types-reflection)을 참고하세요. + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface Model { + username: string; + title: string & Data<'key', 'value'> +} + +const reflection = ReflectionClass.from<Model>(); +reflection.getProperty('title').getData()['key']; //value; +``` + +## Excluded + +각 property는 특정 target에 대해 serialization 과정에서 제외될 수 있습니다. + +```typescript +import { serialize, deserialize, Excluded } from '@deepkit/type'; + +interface Auth { + title: string; + password: string & Excluded<'json'> +} + +const item = deserialize<Auth>({ title: 'Peter', password: 'secret' }); + +item.password; //deserialize의 기본 serializer는 `json`이므로 undefined + +item.password = 'secret'; + +const json = serialize<Auth>(item); +json.password; //serialize의 serializer가 `json`이므로 다시 undefined +``` + +## Embedded + +필드를 embedded type으로 표시합니다. + +```typescript +import { PrimaryKey, Embedded, serialize, deserialize } from '@deepkit/type'; + +interface Address { + street: string; + postalCode: string; + city: string; + country: string; +} + +interface User { + id: number & PrimaryKey; + address: Embedded<Address>; +} + +const user: User +{ + id: 12, + address +: + { + street: 'abc', postalCode + : + '1234', city + : + 'Hamburg', country + : + 'Germany' + } +} +; + +serialize<User>(user); +{ + id: 12, + address_street +: + 'abc', + address_postalCode +: + '1234', + address_city +: + 'Hamburg', + address_country +: + 'Germany' +} + +//deserialize를 위해서는 embedded 구조를 제공해야 합니다 +deserialize<User>({ + id: 12, + address_street: 'abc', + //... +}); +``` + +prefix(기본값은 property 이름)를 변경하는 것도 가능합니다. + +```typescript +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: 'addr_' }>; +} + +serialize<User>(user); +{ + id: 12, + addr_street +: + 'abc', + addr_postalCode +: + '1234', +} + +//혹은 완전히 제거할 수도 있습니다 +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: '' }>; +} + +serialize<User>(user); +{ + id: 12, + street +: + 'abc', + postalCode +: + '1234', +} +``` + +## Entity + +interface에 entity 정보를 annotate합니다. 데이터베이스 문맥에서만 사용됩니다. + +```typescript +import { Entity, PrimaryKey } from '@deepkit/type'; + +interface User extends Entity<{ name: 'user', collection: 'users'> { + id: number & PrimaryKey; + username: string; +} +``` + +## PrimaryKey + +필드를 primary key로 표시합니다. 데이터베이스 문맥에서만 사용됩니다. + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## AutoIncrement + +필드를 auto increment로 표시합니다. 데이터베이스 문맥에서만 사용됩니다. +보통 `PrimaryKey`와 함께 사용됩니다. + +```typescript +import { AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## Reference + +필드를 reference(foreign key)로 표시합니다. 데이터베이스 문맥에서만 사용됩니다. + +```typescript +import { Reference } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; +} +``` + +이 예시에서 `User.group`은 SQL에서 foreign key로도 알려진 owning reference입니다. 이는 `User` 테이블에 `group` 컬럼이 있고, 해당 컬럼이 `Group` 테이블을 참조한다는 뜻입니다. `Group` 테이블은 이 reference의 대상 테이블입니다. + +## BackReference + +필드를 back reference로 표시합니다. 데이터베이스 문맥에서만 사용됩니다. + +```typescript + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; + users: User[] & BackReference; +} +``` + +이 예시에서 `Group.users`는 back reference입니다. 이는 `User` 테이블에 `group` 컬럼이 있고, 해당 컬럼이 `Group` 테이블을 참조한다는 뜻입니다. +`Group`에는 가상 property `users`가 있으며, 조인이 수행되는 데이터베이스 쿼리가 실행되면 `Group` id와 동일한 `group` id를 가진 모든 사용자로 자동 채워집니다. +`users` property는 데이터베이스에 저장되지 않습니다. + +## Index + +필드를 index로 표시합니다. 데이터베이스 문맥에서만 사용됩니다. + +```typescript +import { Index } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Index; +} +``` + +## Unique + +필드를 unique로 표시합니다. 데이터베이스 문맥에서만 사용됩니다. + +```typescript +import { Unique } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Unique; +} +``` + +## DatabaseField + +`DatabaseField`를 사용하면 실제 데이터베이스 컬럼 type, 기본값 등 데이터베이스 특정 옵션을 정의할 수 있습니다. + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & DatabaseField<{ type: 'varchar(255)' }>; +} +``` + +## Validation + +TODO + +[검증 제약 타입](validation.md#validation-constraint-types)을 참고하세요. + +## InlineRuntimeType + +런타임 type을 inline합니다. 고급 케이스에서만 사용됩니다. + +```typescript +import { InlineRuntimeType, ReflectionKind, Type } from '@deepkit/type'; + +const type: Type = { kind: ReflectionKind.string }; + +type Query = { + field: InlineRuntimeType<typeof type>; +} + +const resolved = typeOf<Query>(); // { field: string } +``` + +TypeScript에서 type `Query`는 `{ field: any }`이지만, 런타임에서는 `{ field: string }`입니다. + +이것은 런타임 type을 받아들이는 고도로 커스터마이즈 가능한 시스템을 구축할 때 유용하며, 이를 다양한 다른 경우에 재사용할 수 있습니다. + +## ResetAnnotation + +property의 모든 annotation을 초기화합니다. 고급 케이스에서만 사용됩니다. + +```typescript +import { ResetAnnotation } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} + +interface UserCreationPayload { + id: User['id'] & ResetAnnotation<'primaryKey'>; +} +``` + +### 커스텀 Type Annotation + +사용자 정의 type annotation을 정의할 수 있습니다. + +```typescript +type MyAnnotation = { __meta?: ['myAnnotation'] }; +``` + +관례상, type annotation은 단일 선택적 property `__meta`를 가진 object literal로 정의되며, 이 property는 tuple을 type으로 갖습니다. 이 tuple의 첫 번째 항목은 고유한 이름이고, 그 이후 항목은 임의의 옵션입니다. 이를 통해 type annotation에 추가 옵션을 부여할 수 있습니다. + +```typescript +type AnnotationOption<T extends { title: string }> = { __meta?: ['myAnnotation', T] }; +``` + +type annotation은 교집합 연산자 `&`로 사용됩니다. 하나의 type에 임의 개수의 type annotation을 사용할 수 있습니다. + +```typescript +type Username = string & MyAnnotation; +type Title = string & MyAnnotation & AnnotationOption<{ title: 'Hello' }>; +``` + +type annotation은 `typeOf<T>()`와 `typeAnnotation`의 type object를 통해 읽을 수 있습니다: + +```typescript +import { typeOf, typeAnnotation } from '@deepkit/type'; + +const type = typeOf<Username>(); +const annotation = typeAnnotation.getForName(type, 'myAnnotation'); //[] +``` + +`annotation`의 결과는 type annotation `myAnnotation`이 사용되었다면 옵션이 담긴 배열이며, 사용되지 않았다면 `undefined`입니다. `AnnotationOption`에서 보았듯, type annotation에 추가 옵션이 있다면 전달된 값은 배열에서 찾을 수 있습니다. +`MapName`, `Group`, `Data` 등 이미 제공되는 type annotation은 각각의 annotation object를 가지고 있습니다: + +```typescript +import { typeOf, Group, groupAnnotation } from '@deepkit/type'; + +type Username = string & Group<'a'> & Group<'b'>; + +const type = typeOf<Username>(); +groupAnnotation.getAnnotations(type); //['a', 'b'] +``` + +자세한 내용은 [런타임 타입 리플렉션](./reflection.md)을 참고하세요. \ No newline at end of file diff --git a/website/src/translations/ko/documentation/runtime-types/validation.md b/website/src/translations/ko/documentation/runtime-types/validation.md new file mode 100644 index 000000000..5e0454015 --- /dev/null +++ b/website/src/translations/ko/documentation/runtime-types/validation.md @@ -0,0 +1,549 @@ +# 검증 + +검증은 데이터의 정확성과 무결성을 체계적으로 확인하는 프로세스입니다. 이는 데이터 타입이 기대된 타입과 일치하는지뿐만 아니라, 추가로 정의된 제약 조건들이 충족되는지까지 확인하는 작업을 포함합니다. + +검증은 불확실하거나 신뢰할 수 없는 소스로부터 오는 데이터를 다룰 때 특히 중요합니다. "불확실한(uncertain)" 소스란 데이터의 타입이나 내용이 예측 불가능하여 런타임에 어떤 값이든 될 수 있는 경우를 의미합니다. 대표적인 예로 사용자 입력, HTTP 요청의 데이터(예: query parameters 또는 body), CLI arguments, 프로그램에서 읽어 들이는 파일 등이 있습니다. 이러한 데이터는 잘못된 타입이나 값으로 인해 프로그램 장애가 발생하거나 보안 취약점을 유발할 수 있으므로 본질적으로 위험합니다. + +예를 들어, 어떤 변수가 숫자를 저장해야 한다면 실제로 숫자 값을 담고 있는지 검증하는 것이 중요합니다. 불일치가 생기면 예기치 않은 충돌이나 보안 침해로 이어질 수 있습니다. + +예컨대 HTTP 라우트 컨트롤러를 설계할 때, query parameters, request body 등 모든 사용자 입력을 우선적으로 검증해야 합니다. 특히 TypeScript를 사용하는 환경에서는 type cast를 피하는 것이 중요합니다. 이러한 cast는 오해를 불러일으키고 근본적인 보안 위험을 도입할 수 있습니다. + +```typescript +app.post('/user', function(request) { + const limit = request.body.limit as number; +}); +``` + +코드에서 자주 마주치는 실수 중 하나는 런타임 보안을 제공하지 않는 type cast입니다. 예컨대 변수를 number로 type cast했지만 사용자가 string을 입력하면, 프로그램은 해당 string을 숫자인 것처럼 동작하도록 잘못 인도됩니다. 이러한 간과는 시스템 충돌을 야기하거나 심각한 보안 위협이 될 수 있습니다. 이러한 위험을 완화하기 위해 개발자는 validators와 type guard를 활용할 수 있습니다. 추가로, serializer는 'limit' 같은 변수를 number로 변환하는 역할을 수행할 수 있습니다. 자세한 내용은 Serialization 섹션에서 확인할 수 있습니다. + +검증은 선택 사항이 아니라 견고한 소프트웨어 설계의 핵심 구성 요소입니다. 과유불급이더라도 보수적으로 접근하는 것이 언제나 현명합니다. Deepkit은 이 중요성을 이해하고 다양한 검증 도구를 제공합니다. 더불어 고성능 설계로 실행 시간에 미치는 영향이 최소화됩니다. 기본 원칙으로, 때로는 중복처럼 느껴지더라도 애플리케이션을 보호하기 위해 포괄적인 검증을 적용하십시오. + +HTTP router, RPC abstraction, 데이터베이스 abstraction을 포함한 많은 Deepkit 구성 요소는 내장된 검증 시스템을 제공합니다. 이러한 메커니즘은 자동으로 트리거되어, 수동 개입이 필요 없는 경우가 많습니다. + + +자동 검증이 언제 어떻게 발생하는지에 대한 포괄적인 이해를 위해 다음 챕터들을 참고하세요([CLI](../cli.md), [HTTP](../http.md), [RPC](../rpc.md), [ORM](../orm.md)). +필요한 제약 조건과 데이터 타입을 숙지하십시오. 올바르게 정의된 Parameter는 Deepkit의 자동 검증 기능을 활성화하여 수작업을 줄이고 더 깔끔하고 안전한 코드를 보장합니다. + +## 사용법 + +validator의 기본 기능은 값의 타입을 확인하는 것입니다. 예를 들어 어떤 값이 string인지 여부입니다. 이는 string의 내용이 무엇인지는 중요하지 않고 오직 타입만을 다룹니다. TypeScript에는 string, number, boolean, bigint, objects, classes, interface, generics, mapped types 등 다양한 타입이 존재합니다. TypeScript의 강력한 type system 덕분에 매우 다양한 타입이 가능합니다. + +JavaScript 자체에서는 원시 타입을 `typeof` 연산자로 판별할 수 있습니다. 그러나 interface, mapped types, generic set/map 같은 더 복잡한 타입의 경우 더 이상 쉽지 않으며 `@deepkit/type` 같은 validator 라이브러리가 필요해집니다. Deepkit은 모든 TypeScript 타입을 우회 없이 직접 검증할 수 있는 유일한 솔루션입니다. + + + +Deepkit에서는 `validate`, `is`, `assert` Function을 사용해 타입 검증을 할 수 있습니다. +`is` Function은 이른바 type guard이고 `assert`는 type assertion입니다. 둘은 다음 섹션에서 설명합니다. +`validate` Function은 발견된 오류들의 배열을 반환하며, 성공 시에는 빈 배열을 반환합니다. 이 배열의 각 항목은 정확한 error code와 error message, 그리고 객체나 배열 같은 더 복잡한 타입이 검증될 때의 path를 설명합니다. + +세 Function 모두 대체로 같은 방식으로 사용됩니다. 첫 번째 type argument로 타입을 지정하거나 참조하고, 첫 번째 Function argument로 데이터를 전달합니다. + +```typescript +import { validate, is, assert } from '@deepkit/type'; + +const errors = validate<string>('abc'); //[] +const errors = validate<string>(123); //[{code: 'type', message: 'Not a string'}] + +if (is<string>(value)) { + // value는 이제 string임이 보장됩니다 +} + +function doSomething(value: any) { + assert<string>(value); //유효하지 않은 데이터일 경우 예외를 던짐 + + // value는 이제 string임이 보장됩니다 +} +``` + +Class나 Interface 같은 더 복잡한 Type으로 작업한다면, 배열에는 여러 항목이 포함될 수 있습니다. + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>(undefined); //[{code: 'type', message: 'Not a object'}] + +validate<User>({}); +//[ +// {path: 'id', code: 'type', message: 'Not a number'}], +// {path: 'username', code: 'type', message: 'Not a string'}], +//] +``` + +validator는 깊은 재귀 타입도 지원합니다. 이때 path는 점(dot)으로 구분됩니다. + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; + supervisor?: User; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>({id: 1, username: 'Joe', supervisor: {}}); +//[ +// {path: 'supervisor.id', code: 'type', message: 'Not a number'}], +// {path: 'supervisor.username', code: 'type', message: 'Not a string'}], +//] +``` + +TypeScript가 제공하는 이점을 적극 활용하세요. 예를 들어 `User` 같은 더 복잡한 Type은 여러 곳에서 재사용할 수 있으며, 매번 다시 선언할 필요가 없습니다. 예컨대 `id`를 제외한 `User`를 검증하려면 TypeScript 유틸리티를 사용해 빠르고 효율적으로 파생 subtype을 만들 수 있습니다. 이는 DRY(Don't Repeat Yourself) 정신에 부합합니다. + +```typescript +type UserWithoutId = Omit<User, 'id'>; + +validate<UserWithoutId>({username: 'Joe'}); //유효함! +``` + +Deepkit은 런타임에 이와 같은 방식으로 TypeScript의 Type에 접근할 수 있는 유일한 주요 프레임워크입니다. 프론트엔드와 백엔드에서 Type을 공용으로 사용하고 싶다면, Type을 별도 파일로 분리하여 어디서든 import할 수 있습니다. 이 옵션을 활용해 코드를 효율적이고 깔끔하게 유지하십시오. + +## Type Cast는 안전하지 않음 + +TypeScript에서 type cast는(type guard와 달리) 런타임의 구조물이 아니라 type system 내부에서만 처리됩니다. 이는 알 수 없는 데이터에 타입을 부여하는 안전한 방법이 아닙니다. + +```typescript +const data: any = ...; + +const username = data.username as string; + +if (username.startsWith('@')) { //충돌(crash)할 수 있음 +} +``` + +`as string` 코드는 안전하지 않습니다. 변수 `data`는 실제로 어떤 값이든 될 수 있으며, 예를 들어 `{username: 123}` 또는 `{}`일 수도 있습니다. 그 결과 `username`은 string이 아니라 완전히 다른 것이 되어 `username.startsWith('@')` 코드가 Error를 유발하고, 최선의 경우 프로그램이 충돌하며, 최악의 경우 보안 취약점이 생길 수 있습니다. +런타임에 `data`가 string 타입의 `username` Property를 가진다는 것을 보장하려면, 반드시 type guard를 사용해야 합니다. + +type guard는 TypeScript에 전달된 데이터가 런타임에 어떤 타입임이 보장되는지에 대한 힌트를 주는 Function입니다. 이 지식을 바탕으로 TypeScript는 코드 진행에 따라 타입을 "좁혀(narrow)" 줍니다. 예를 들어, `any`를 안전하게 string이나 다른 타입으로 만들 수 있습니다. 즉, 타입을 알 수 없는(`any` 또는 `unknown`) 데이터가 있다면, type guard가 데이터 자체를 기반으로 보다 정확하게 타입을 좁히는 데 도움이 됩니다. 그러나 type guard의 안전성은 구현의 정확성에 달려 있습니다. 실수가 있다면 근본적인 가정이 틀렸음이 드러나 심각한 문제로 이어질 수 있습니다. + +<a name="type-guard"></a> + +## Type-Guard + +위에서 사용한 `User` Type에 대한 type guard는 가장 단순하게 다음과 같이 생겼을 수 있습니다. 위에서 설명한 NaN 관련 특수 사항은 여기 포함되어 있지 않으므로 이 type guard는 완전히 정확하지 않다는 점에 유의하세요. + +```typescript +function isUser(data: any): data is User { + return 'object' === typeof data + && 'number' === typeof data.id + && 'string' === typeof data.username; +} + +isUser({}); //false + +isUser({id: 1, username: 'Joe'}); //true +``` + +type guard는 항상 Boolean을 반환하며 보통 If 문에서 바로 사용됩니다. + +```typescript +const data: any = await fetch('/user/1'); + +if (isUser(data)) { + data.id; //안전하게 접근할 수 있으며 number입니다 +} +``` + +특히 더 복잡한 Type에 대해 각 type guard마다 별도의 Function을 작성하고, 타입이 바뀔 때마다 이를 수정하는 것은 매우 번거롭고 오류가 발생하기 쉽고 비효율적입니다. 따라서 Deepkit은 모든 TypeScript Type에 대해 자동으로 Type-Guard를 제공하는 `is` Function을 제공합니다. 이는 위에서 언급한 NaN 문제 같은 특수 사항도 자동으로 고려합니다. `is` Function은 `validate`와 동일한 일을 하지만, 오류 배열 대신 단순히 boolean을 반환합니다. + +```typescript +import { is } from '@deepkit/type'; + +is<string>('abc'); //true +is<string>(123); //false + + +const data: any = await fetch('/user/1'); + +if (is<User>(data)) { + //data는 이제 User 타입임이 보장됩니다 +} +``` + +자주 쓰이는 패턴으로, 검증 실패 시 즉시 Error를 반환하여 이후 코드가 실행되지 않게 하는 방법이 있습니다. 이는 코드 전체 흐름을 바꾸지 않고 다양한 곳에서 사용할 수 있습니다. + +```typescript +function addUser(data: any): void { + if (!is<User>(data)) throw new TypeError('No user given'); + + //data는 이제 User 타입임이 보장됩니다 +} +``` + +대안으로 TypeScript type assertion을 사용할 수 있습니다. `assert` Function은 주어진 데이터가 타입에 올바르게 검증되지 않으면 자동으로 Error를 던집니다. TypeScript type assertion을 구분하는 이 Function의 특별한 시그니처 덕분에 TypeScript는 전달된 변수를 자동으로 좁혀줍니다. + +```typescript +import { assert } from '@deepkit/type'; + +function addUser(data: any): void { + assert<User>(data); //유효하지 않은 데이터라면 예외를 던짐 + + //data는 이제 User 타입임이 보장됩니다 +} +``` + +여기서도 TypeScript가 제공하는 이점을 활용하세요. Type은 다양한 TypeScript 기능을 사용해 재사용하거나 커스터마이즈할 수 있습니다. + +<a name="error-reporting"></a> + +## 오류 보고 + +`is`, `assert`, `validates` Function은 결과로 boolean을 반환합니다. 실패한 검증 규칙에 대한 정확한 정보를 얻으려면 `validate` Function을 사용할 수 있습니다. 모든 검증이 성공하면 빈 배열을 반환합니다. 오류가 있을 경우 다음 구조의 하나 이상의 항목이 배열에 포함됩니다: + +```typescript +interface ValidationErrorItem { + /** + * 해당 Property까지의 경로. 점으로 구분된 깊은 경로일 수 있습니다. + */ + path: string; + /** + * 이 오류를 식별하고 번역하는 데 사용할 수 있는 소문자 error code. + */ + code: string, + /** + * 오류에 대한 자유 형식의 텍스트. + */ + message: string, +} +``` + +이 Function은 첫 번째 type argument로 임의의 TypeScript Type을 받고, 첫 번째 argument로 검증할 데이터를 받습니다. + +```typescript +import { validate } from '@deepkit/type'; + +validate<string>('Hello'); //[] +validate<string>(123); //[{code: 'type', message: 'Not a string', path: ''}] + +validate<number>(123); //[] +validate<number>('Hello'); //[{code: 'type', message: 'Not a number', path: ''}] +``` + +interface, class, generics 같은 복잡한 Type도 사용할 수 있습니다. + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>(undefined); //[{code: 'type', message: 'Not an object', path: ''}] +validate<User>({}); //[{code: 'type', message: 'Not a number', path: 'id'}] +validate<User>({id: 1}); //[{code: 'type', message: 'Not a string', path: 'username'}] +validate<User>({id: 1, username: 'Peter'}); //[] +``` + +<a name="constraints"></a> + +## 제약 조건 + +타입을 확인하는 것 외에도, 임의의 추가 제약 조건을 타입에 추가할 수 있습니다. 이러한 추가 내용 제약의 검증은 타입 자체가 검증된 이후 자동으로 수행됩니다. 이는 `validate`, `is`, `assert` 같은 모든 검증 Function에서 이루어집니다. +예를 들어 문자열의 최소 또는 최대 길이를 요구하는 것을 제약으로 둘 수 있습니다. 이러한 제약은 [타입 애너테이션](./types.md)을 통해 실제 타입에 추가됩니다. 사용할 수 있는 애너테이션은 매우 다양합니다. 확장된 요구 사항이 있는 경우 자체 애너테이션을 정의하여 마음껏 사용할 수 있습니다. + +```typescript +import { MinLength } from '@deepkit/type'; + +type Username = string & MinLength<3>; +``` + +`&`를 사용해 원하는 만큼의 타입 애너테이션을 실제 타입에 추가할 수 있습니다. 이렇게 만들어진 결과(여기서는 `username`)는 모든 검증 Function에서, 그리고 다른 타입에서도 사용할 수 있습니다. + +```typescript +import { is } from '@deepkit/type'; + +is<Username>('ab'); //false, 최소 길이는 3 +is<Username>('Joe'); //true + +interface User { + id: number; + username: Username; +} + +is<User>({id: 1, username: 'ab'}); //false, 최소 길이는 3 +is<User>({id: 1, username: 'Joe'}); //true +``` + +`validate` Function은 제약으로부터 유용한 error message를 제공합니다. + +```typescript +import { validate } from '@deepkit/type'; + +const errors = validate<Username>('xb'); +//[{ code: 'minLength', message: `Min length is 3` }] +``` + +이 정보는 예를 들어 폼에서 자동으로 훌륭하게 표시될 수 있으며, `code`를 통해 번역될 수 있습니다. 객체와 배열에 대한 기존 path를 통해, 폼의 각 필드는 해당 오류를 골라내어 표시할 수 있습니다. + +```typescript +validate<User>({id: 1, username: 'ab'}); +//{ path: 'username', code: 'minLength', message: `Min length is 3` } +``` + +자주 유용한 사용 사례로 RegExp 제약을 사용해 email을 정의하는 것이 있습니다. 한 번 타입을 정의하면 어디서든 사용할 수 있습니다. + +```typescript +export const emailRegexp = /^\S+@\S+$/; +type Email = string & Pattern<typeof emailRegexp> + +is<Email>('abc'); //false +is<Email>('joe@example.com'); //true +``` + +원하는 만큼의 제약을 추가할 수 있습니다. + +```typescript +type ID = number & Positive & Maximum<1000>; + +is<ID>(-1); //false +is<ID>(123); //true +is<ID>(1001); //true +``` + +### 제약 타입 + +#### Validate<typeof myValidator> + +사용자 정의 validator Function을 사용한 검증. 자세한 내용은 다음 섹션 Custom Validator를 참고하세요. + +```typescript +import { ValidatorError, Validate } from '@deepkit/type'; + +function startsWith(v: string) { + return (value: any) => { + const valid = 'string' === typeof value && value.startsWith(v); + return valid ? undefined : new ValidatorError('startsWith', `Does not start with ${v}`); + }; +} + +type T = string & Validate<typeof startsWith, 'abc'>; +``` + +#### Pattern<typeof myRegexp> + +정규식을 검증 패턴으로 정의합니다. 보통 E-Mail 검증이나 더 복잡한 내용 검증에 사용됩니다. + +```typescript +import { Pattern } from '@deepkit/type'; + +const myRegExp = /[a-zA-Z]+/; +type T = string & Pattern<typeof myRegExp> +``` + +#### Alpha + +알파 문자(a-Z)에 대한 검증. + +```typescript +import { Alpha } from '@deepkit/type'; + +type T = string & Alpha; +``` + + +#### Alphanumeric + +알파와 숫자 문자의 조합에 대한 검증. + +```typescript +import { Alphanumeric } from '@deepkit/type'; + +type T = string & Alphanumeric; +``` + + +#### Ascii + +ASCII 문자에 대한 검증. + +```typescript +import { Ascii } from '@deepkit/type'; + +type T = string & Ascii; +``` + + +#### Decimal<number, number> + +문자열이 0.1, .3, 1.1, 1.00003, 4.0 등과 같은 십진수를 나타내는지에 대한 검증. + +```typescript +import { Decimal } from '@deepkit/type'; + +type T = string & Decimal<1, 2>; +``` + + +#### MultipleOf<number> + +주어진 수의 배수인 number에 대한 검증. + +```typescript +import { MultipleOf } from '@deepkit/type'; + +type T = number & MultipleOf<3>; +``` + + +#### MinLength<number>, MaxLength<number>, MinMax<number, number> + +배열 또는 문자열의 최소/최대 길이에 대한 검증. + +```typescript +import { MinLength, MaxLength, MinMax } from '@deepkit/type'; + +type T = any[] & MinLength<1>; + +type T = string & MinLength<3> & MaxLength<16>; + +type T = string & MinMax<3, 16>; +``` + +#### Includes<'any'> Excludes<'any'> + +배열 항목 또는 부분 문자열의 포함/제외에 대한 검증 + +```typescript +import { Includes, Excludes } from '@deepkit/type'; + +type T = any[] & Includes<'abc'>; +type T = string & Excludes<' '>; +``` + +#### Minimum<number>, Maximum<number> + +값이 주어진 수의 최소/최대인지에 대한 검증. `>=` 및 `<=`와 동일합니다. + +```typescript +import { Minimum, Maximum, MinMax } from '@deepkit/type'; + +type T = number & Minimum<10>; +type T = number & Minimum<10> & Maximum<1000>; + +type T = number & MinMax<10, 1000>; +``` + +#### ExclusiveMinimum<number>, ExclusiveMaximum<number> + +minimum/maximum과 동일하지만 값 자체는 제외합니다. `>` 및 `<`와 동일합니다. + +```typescript +import { ExclusiveMinimum, ExclusiveMaximum } from '@deepkit/type'; + +type T = number & ExclusiveMinimum<10>; +type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>; +``` + + +#### Positive, Negative, PositiveNoZero, NegativeNoZero + +값이 양수/음수인지에 대한 검증. + +```typescript +import { Positive, Negative } from '@deepkit/type'; + +type T = number & Positive; +type T = number & Negative; +``` + + +#### BeforeNow, AfterNow + +현재(new Date)와 비교한 날짜 값에 대한 검증. + +```typescript +import { BeforeNow, AfterNow } from '@deepkit/type'; + +type T = Date & BeforeNow; +type T = Date & AfterNow; +``` + +#### Email + +`/^\S+@\S+$/`를 통한 간단한 email 정규식 검증. 자동으로 `string`이므로 `string & Email`을 할 필요가 없습니다. + +```typescript +import { Email } from '@deepkit/type'; + +type T = Email; +``` + +#### integer + +number가 올바른 범위의 정수인지 보장합니다. 자동으로 `number`이므로 `number & integer`를 할 필요가 없습니다. + +```typescript +import { integer, uint8, uint16, uint32, + int8, int16, int32 } from '@deepkit/type'; + +type T = integer; +type T = uint8; +type T = uint16; +type T = uint32; +type T = int8; +type T = int16; +type T = int32; +``` + +자세한 내용은 Special types: integer/floats를 참고하세요. + +### Custom validator + +내장 validator로 충분하지 않은 경우, `Validate` 데코레이터를 통해 사용자 정의 검증 Function을 만들고 사용할 수 있습니다. + +```typescript +import { ValidatorError, Validate, Type, validates, validate } + from '@deepkit/type'; + +function titleValidation(value: string, type: Type) { + value = value.trim(); + if (value.length < 5) { + return new ValidatorError('tooShort', 'Value is too short'); + } +} + +interface Article { + id: number; + title: string & Validate<typeof titleValidation>; +} + +console.log(validates<Article>({id: 1})); //false +console.log(validates<Article>({id: 1, title: 'Peter'})); //true +console.log(validates<Article>({id: 1, title: ' Pe '})); //false +console.log(validate<Article>({id: 1, title: ' Pe '})); //[ValidationErrorItem] +``` + +사용자 정의 검증 Function은 모든 내장 타입 validator가 호출된 다음에 실행된다는 점에 유의하세요. 하나의 validator가 실패하면, 현재 타입의 이후 validator는 모두 건너뜁니다. 타입당 하나의 실패만 발생합니다. + +#### Generic Validator + +Validator Function에서는 type object를 사용할 수 있어, validator가 사용하는 타입에 대한 더 많은 정보를 얻을 수 있습니다. 또한 validate 타입에 전달해야 하며 validator를 구성 가능하게 만드는 임의의 validator 옵션을 정의할 수도 있습니다. 이 정보와 상위 참조를 통해 강력한 generic validator를 만들 수 있습니다. + +```typescript +import { ValidatorError, Validate, Type, is, validate } + from '@deepkit/type'; + +function startsWith(value: any, type: Type, chars: string) { + const valid = 'string' === typeof value && value.startsWith(chars); + if (!valid) { + return new ValidatorError('startsWith', 'Does not start with ' + chars) + } +} + +type MyType = string & Validate<typeof startsWith, 'a'>; + +is<MyType>('aah'); //true +is<MyType>('nope'); //false + +const errors = validate<MyType>('nope'); +//[{ path: '', code: 'startsWith', message: `Does not start with a` }]); +``` \ No newline at end of file diff --git a/website/src/translations/ko/state.json b/website/src/translations/ko/state.json new file mode 100644 index 000000000..73e29dfb6 --- /dev/null +++ b/website/src/translations/ko/state.json @@ -0,0 +1,116 @@ +{ + "index.md": "29488f37f0c90cb66e32023aa3869f6081dab42590def9196514de8e51e977de", + "introduction.md": "7159b93c7cf42ed98bd0a86ed2b0100884e1eae755e56d8392e9a4dfbe306dd0", + "app.md": "f1f4383a395605c4cd1c36314b61afeebc6369e8c624080ba4f005bd064db99d", + "app/arguments.md": "009c2438d897ea0f25d569d9b3b9654e55d1a01ad3c04bf8b4c512a1b9789f59", + "app/dependency-injection.md": "b89fd5d14b2e0180a5c29f31e79ecc4112d328d9efe89f4e5a3ca8caa88129f5", + "app/modules.md": "9d0c92e9db99c4e96c00152a51f69efa49c974860e33937aec6144d10795dc27", + "app/services.md": "9df90a136ffed203714a4427137ba9bfda6901eb77c28dfe0d8c305f135c06c3", + "app/events.md": "45af019cd3a41e6e5c4987b6d7980d5103afb20a889aba7c9a8d04cae73b3dba", + "app/logger.md": "467d21c830908bc4a49c3a3d80dd6c9207c3403977a693788990b04d89f2c8d6", + "app/configuration.md": "5cd9710e8c7037e4af0a43f47095332fca70a9c0a1e267f249d0f89e1d0c995e", + "framework.md": "7d23eb8115535150c547adc60d8677c9e74f7aa6d0f9ba2aa2c34dd3c61c1f72", + "framework/database.md": "3082875ba15ff97becd94966634dc57a4e3a96e0e09db1bd1a901faeecfc3a9f", + "framework/testing.md": "f22048a69772b752d7174645e25708288f67721ac4f16774e7c3786d9143691f", + "framework/deployment.md": "9f43e70a78caf28a58a76e6b6f4b654dca8e83fef9a5dfacd0544c9a40251ee0", + "framework/public.md": "014cf4e0702d543c6aa086a1256e024d63a06fe761f4352e06ba953a3ae5d39f", + "runtime-types.md": "ddb2b9e67054ede0ca0937044b4876882326ea421d7a9919633ae8187782bff7", + "runtime-types/getting-started.md": "437cb0b8ba80b036e023719d8bd56ad22d5414420199b9fa7561afba1f68b65a", + "runtime-types/types.md": "f8b43c9534f850990aee6e57dfcd5bffdaf087d953b2edc3f95dea5b9cc53b6d", + "runtime-types/reflection.md": "5b96135eeb9281f65ca5c365d4697868a31ce2a85b141e0283070ac3bec8dae9", + "runtime-types/serialization.md": "9d1d7adac33dc116244b6edb7cf37c88c7d7657c3692d3e354e04c48b71d7561", + "runtime-types/validation.md": "9ba4b9f65b3f2c882e1df373411652df407b1ee9300faed54d6f4b62b4a0f986", + "runtime-types/extend.md": "2b88f476dac2c88ffcd05719177b9c5e43281b65cd3925698df14483f3de4792", + "runtime-types/custom-serializer.md": "b3da230cee1630ea1eb7d89f7b3e241726c3b956dffb6a8a3f94da0a2ea8ef12", + "runtime-types/external-types.md": "216d15bd8b956f8821bd1c3c86571550beabe9b769237514b232b4dd8af72714", + "runtime-types/bytecode.md": "b22322df3dcc093da4d776a1b68d3d11ae5e8c8053723f785ad2a25c6a2e8d2e", + "dependency-injection.md": "c1ddf1c3c6dc56783c7e282605f5b3b0ea50f81cd8d614ff77a6459e3893f3d4", + "dependency-injection/getting-started.md": "4a381a82a5b5aa208c44d08334b6b7ae8ee129878f1a212b08230371cee99556", + "dependency-injection/providers.md": "64ae199092c89687a393ec3b50722c746e1048fb9aceed2c5f4d61150c43c573", + "dependency-injection/injection.md": "399be234a76ea3575f53a1c1bf04ab7368b4d9711b3811f5be69ccdb275da80c", + "dependency-injection/configuration.md": "9f84fa97310995897afafdf2b6b5376858ad22722d25a19562b1f70051a41009", + "dependency-injection/scopes.md": "7886d8e715ef18a5b058a89fe099cf4f5ce2947495c7f1fb4a8fe1908ec0cf59", + "filesystem.md": "97fc7c9f028efb934b9ad864135420007cd1910c7b8a7b3b8bdc86b0b8e90cdf", + "filesystem/app.md": "f7aa47e42604191de2d549f1f8e5e9582394e55da0c8d470147783b62a6a383d", + "filesystem/local.md": "e62660fe771d5c22c1414a055bc96e77a92da04e780f8853be9a5d327a2077b2", + "filesystem/memory.md": "67e061e990066271c4e6d92e1552319a8f1d3e2b5b97b2a26a9dc2e88858e906", + "filesystem/database.md": "8d1132ef3880a90e48e27598514ce6f75eed6328658e7c29b3af9509b8735859", + "filesystem/aws-s3.md": "989c857dbe659e4c6a765442143945402ed49db36248958a123c3d11649b4148", + "filesystem/ftp.md": "3400d186b86e1abb2823a0d64a143720099b2d5ba9c9e59e78ce1cc24d74df65", + "filesystem/sftp.md": "f54ebc5db81bf8cc4e3e21e922168db5456ba9912abbcb456a17d7603ffee4db", + "filesystem/google-storage.md": "c20d8abfc8e10c26844bc8a154ca8eacdf202c812cdde45911e30086943429cb", + "broker.md": "0e97c1ea8ba83e929555018e47a2e7ea9a4d951a2eda3fdd3c68ca2e83dc9c99", + "broker/cache.md": "cfbc3dc50876e5951c848a60a82c5b83d34dc351cb325a64850e6910a774ba34", + "broker/message-bus.md": "8fec0f694e08bd4e894ddafa8d97ce4183b8713d1d9be93dcb1bde4df7ac700c", + "broker/message-queue.md": "cbc54b3d2ea7aba249948f06d8f622defe7cfcae9ef5e0fa48f97b876e8346fb", + "broker/atomic-locks.md": "90703416ba1fec9f5c14eee4972ee78b983d126544737f2801188962741d87c8", + "broker/key-value.md": "da10a448f38971d72963500f4cfc06ad8253d79808d877453f6e5734cdf30070", + "http.md": "2c20d0e955afe136ada302e197f176255aac7d941760c208f0dc75c2f3cf258a", + "http/getting-started.md": "59605a5fe0ca617bb70e3fd0014340b9b57a88e8e91fac9bdc3eaf0a8f8f11c1", + "http/input-output.md": "c225ccfafb21816bd804a96110e69301baa0dbb57304baed8a7595b771f46f77", + "http/views.md": "62ce86b45d849b7946608aeea23f88f1078d74f971703709c98f8ad476944893", + "http/dependency-injection.md": "65d42f23a838af794cc43e60aaa2ae6cd3d72e2d0fc275065e65c6c639a6e534", + "http/events.md": "aa47d884f82e724f1dcb5b33b10624bef1e9a04b84f97c4db5a8d3f8da6bd33b", + "http/middleware.md": "9485a78862a08ff1de9cfd52b89f095676e70e058ec732b8e5617d96e961372a", + "http/security.md": "cde7cb77eb3b2aa51456d20d29a06ea7ab205b40ef9ad586dbfd136cd498019c", + "rpc.md": "cf15217c23ae52a86ab7dcf628c668d5d06b9720ef4ddb3717f02994a3d507f7", + "rpc/getting-started.md": "b09000d9e2d9dfe6527b8a42cbec75ae2206284d4395b75e21a8e1c596430ba9", + "rpc/dependency-injection.md": "c8e5218600779f3c3c4fcdccbe7ab8add343f8a8c7f2847dc3b80da96d1604e9", + "rpc/security.md": "790bb3ff8ef236250d29d16b55e8b685be273f52accb3a368be71eed4a8e639d", + "rpc/errors.md": "7785519ce38ddd349353e71bcc62bb5ba74582f52c17378508f4c6e9051609a9", + "rpc/transport.md": "9561d6ccbfeaf511b6a914fd3d7e6f4b84411f70938ad639665f113e469e1991", + "orm.md": "e922d5a0e28aa3753e5332418f2802f1ae62596d17ae96f46d3deca59c4e7c0b", + "orm/getting-started.md": "56e4f565f748aaf043ce8710fc5a58402fb5889366d1478a267e26628ff4e0f6", + "orm/entity.md": "5fccac031df6723dc0e6bdc9ef7ecd4268631b3b14854661d8ce6dc918f9959c", + "orm/session.md": "88c774506bcca58e5c272f59b844846ebe4efe5ab2e3a367528764b0dc5dfdee", + "orm/query.md": "540a355c2c1666c2a4d9e7c8cc202994b732b17408508fd6944dde1f62a99646", + "orm/transactions.md": "b843d35bab46190b24a79f05950b6a301349bf539c01aae7b90716f5d0153b41", + "orm/inheritance.md": "943f367861fcaebffe70d16c4d795a739b16537bed0ca602d0343e0684ebc0ae", + "orm/relations.md": "9cfd7b3516d704e091207b0d78995b26df6d5b88b1c8181ee021ee0d7767795a", + "orm/events.md": "220d9066075c3a82e354dc8e6fcf82db649e440b8c3df0c0d2722ac5bec9dcfd", + "orm/migrations.md": "2ee65d0ccd58635e9cd115a3abf83b2488ddc925a7d5f5b25d8844bcedc107e6", + "orm/orm-browser.md": "ee8940fa282c2d42861ce203524277f4672c880a3e79cffdc978c55e795cec1b", + "orm/raw-access.md": "55c6e5a40386cff597293293536be5b47b082122432156b50bf06b386d3bb694", + "orm/seeding.md": "92bad8e262e1afdc818b39e6aa91c50a9016ad37da273defb7caf575821e6539", + "orm/composite-primary-key.md": "155cb42a4dcd9deffaa67aa30369d86c37826a28740977fe1905ef0c61bb9129", + "orm/plugin-soft-delete.md": "afa2c7f42833bddda5f7e116a3e7eabfc7183d112812e6c4e8c3a802aca5c3ff", + "package/angular-ssr.md": "e7e74dfdd8f957e82061c449938c2e1288031b976921e54e28b4f0d35b5d491d", + "package/api-console.md": "82d5322b37c517da883f567a18d4f64ecbcd61570300ac337e5e3c1473359880", + "package/app.md": "e60ef9f596cc6df26f490350a0001a912a715d463c762ed09ca6bf796e4cb4b3", + "package/bench.md": "f7a06c685a2f05aa397a2f271bec2a98c729c4273591e6996381b420a57116df", + "package/broker.md": "e46abbefd581c101d77fdf35151eddeeba32074d24f6e7b737841c3cefa2b76f", + "package/broker-redis.md": "67bbf67a50265ada9755bf093cc3d566b8ff21dd10a2cd1f3ed5f589826aac1a", + "package/bson.md": "dbbadeca42bacda8a7cb2e7cce55e9627463537cf16de4458dd029163575063e", + "package/bun.md": "a5627944c66b654c3b64e380fa58acebf0a22f4b045ae218880d882973f457eb", + "package/core.md": "fabeee88c765e0a0713cfafa7cc502bd4f04fa4c69467a8a9985d7d74b5b963e", + "package/core-rxjs.md": "42784cd0fa8c85d0473753e1cee7b769bf03d4d220f18add821bfa402624619f", + "package/devtool.md": "9fa98e55994ae65e50032d11c9c01f7fb52fbe0dec22cc9b2b6d5dbbe67af122", + "package/event.md": "4bcb6f33e7b788d4f2db94f8871ccded00c6523cd80bcf593d732293d207e148", + "package/filesystem.md": "0c2634e9f2f938d1bec04d201e449642f2573c1e0790c1fb783dccea91b4fa63", + "package/filesystem-aws-s3.md": "40cfd7a7cd0d0f495d9e76f693792859accd6e56a92d6defbff0458cd55ee73d", + "package/filesystem-database.md": "61848eaf206556fbc5d317eb7a1286288bc48329ad209b7dc4ce6daaa1ce374f", + "package/filesystem-ftp.md": "fdbd9659eb784a5f7cc8219a59c647b7646c7de939e424fe23ea9c6fe0306d3a", + "package/filesystem-google.md": "549623afd3bfa25d793758893360902751a21b0b6ae900e27448c8cb2bc8593c", + "package/filesystem-sftp.md": "b2966556da65453da7b98a6900667b2fb97bab48c986bbdae667c172fec44b41", + "package/framework.md": "027c9d986ec7e32a3b93a857f4cdb80367d17b54faf59c7d1620d8a21f7c4628", + "package/http.md": "159c8f0553a1cd8f71598ef7128a78525b7ad55999b8b2270acbdd05435352fd", + "package/injector.md": "f01def47d678021bb90a48e20d7e94ce18e865422affcf39314d66d04218b11e", + "package/logger.md": "0336bedc41fda399389fbb9085032091312973f33e3ec9bd4ed46aa931469100", + "package/mongo.md": "359d07c8c17320d28840fbdfa0c3eb219f5eedd65b14866ab3ac64d25d502c6f", + "package/mysql.md": "0bde461414a0669797ff3c9c1c1ab6954dbb1bd6d1fd11a4d916fa8cb41ea683", + "package/orm.md": "2e622c77f4477c7e69f330b68b3253e3d54cf9ab4c124935269a677c31753919", + "package/orm-browser.md": "534cb7f207f14d425a3ccd03e2ad038137d3732ea1fd51a67019b315e02e766c", + "package/postgres.md": "9296b10815117f3f96af3171c36a554b8fb5033b4328d956338ce69adcb18d04", + "package/rpc.md": "c455f9ab0212e27b208b29948c9979feb9fd05143248474f91d5dce0b6c1d236", + "package/rpc-tcp.md": "4924f5e05bec8a288008315edef65bcc1ee5d940cc87b0126b82a9f188b78079", + "package/run.md": "16f7e3360ffcabe09bda174e6c8ed64055a869c41d2355c4d55344fde25ee411", + "package/sql.md": "8b4046cc206ff85449595ed7debce715e487097a3e9446f1841b7ef213c932dd", + "package/sqlite.md": "64fc4711e3557b9453300d2a99f01d69ea640d728483984ff35ea13142a79efb", + "package/stopwatch.md": "f8dc8d14b28ee81a00ac9c30f9278ce70ece11837dd132d47dfd0e70ed7fa8af", + "package/template.md": "135fe797c2a4309e2257bd118218927ce1b1d1265fa2297f5f02c75b4b62c5dc", + "package/topsort.md": "a0c2258b8859c5c7d864758f27699835d2017a630d3855eb08df4d4e36024def", + "package/type.md": "7d28ae8aca6dbbd0439bd7c778f5871d3ba7305660edeefa8fb7aa5ae80bfb3d", + "package/type-compiler.md": "819bcb710c7cc610238a69b37eb6809d693ccce84f3dc239642591d0cfcf9426", + "package/vite.md": "df1a5256d86a3560f3f0384122779255b9afe038c0c0eca4f705a23c336b51e2", + "package/workflow.md": "ebe03caf6abcd0ea512dd560ccf54c9fb34798b66c87780e6c24ce295cf8e76d" +} \ No newline at end of file diff --git a/website/src/translations/zh/basics.json b/website/src/translations/zh/basics.json new file mode 100644 index 000000000..ae6d7391b --- /dev/null +++ b/website/src/translations/zh/basics.json @@ -0,0 +1,160 @@ +{ + "Chapters": "章节", + "Blog": "博客", + "Docs": "文档", + "Overview": "概述", + "Introduction": "介绍", + "App": "应用", + "Getting started": "快速开始", + "Arguments & Flags": "参数和标志", + "Dependency Injection": "依赖注入", + "Modules": "模块", + "Services": "服务", + "Events": "事件", + "Logger": "日志记录器", + "Configuration": "配置", + "Framework": "框架", + "Database": "数据库", + "Testing": "测试", + "Deployment": "部署", + "Public Assets": "公共资产", + "Runtime Types": "运行时类型", + "Type Annotations": "类型注解", + "Reflection": "反射", + "Serialization": "序列化", + "Validation": "验证", + "Extend": "扩展", + "Custom serializer": "自定义序列化器", + "External Types": "外部类型", + "Bytecode": "字节码", + "Providers": "提供者", + "Injection": "注入", + "Scopes": "范围", + "Filesystem": "文件系统", + "Local": "本地", + "Memory": "内存", + "AWS S3": "AWS S3", + "FTP": "FTP", + "sFTP (SSH)": "sFTP (SSH)", + "Google Storage": "谷歌存储", + "Broker": "代理", + "Cache": "缓存", + "Message Bus": "消息总线", + "Message Queue": "消息队列", + "Atomic Locks": "原子锁", + "Key Value": "键值", + "HTTP": "HTTP", + "Input & Output": "输入和输出", + "Views": "视图", + "Middleware": "中间件", + "Security": "安全", + "RPC": "远程过程调用", + "Errors": "错误", + "Transport": "传输", + "Database ORM": "数据库ORM", + "Entity": "实体", + "Session": "会话", + "Query": "查询", + "Transaction": "事务", + "Inheritance": "继承", + "Relations": "关系", + "Migrations": "迁移", + "ORM Browser": "ORM浏览器", + "Raw Access": "原始访问", + "Seeding": "数据种子", + "Composite primary key": "复合主键", + "Soft-Delete": "软删除", + "Desktop UI": "桌面UI", + "Styles": "样式", + "Adaptive Container": "自适应容器", + "Button": "按钮", + "Button group": "按钮组", + "Dialog": "对话框", + "Drag": "拖拽", + "Dropdown": "下拉菜单", + "Icons": "图标", + "Input": "输入", + "Menu": "菜单", + "Slider": "滑块", + "Radio": "单选", + "Select": "选择", + "Checkbox": "复选框", + "List": "列表", + "Table": "表格", + "Tabs": "标签页", + "Window": "窗口", + "Window Toolbar": "窗口工具栏", + "API": "API", + "Composition": "组合", + "Angular SSR": "Angular SSR", + "Infrastructure": "基础设施", + "RPC TCP": "RPC TCP", + "Broker Redis": "代理Redis", + "Filesystem FTP": "文件系统FTP", + "Filesystem SFTP": "文件系统SFTP", + "Filesystem S3": "文件系统 S3", + "Filesystem Google": "文件系统谷歌", + "Filesystem Database": "文件系统数据库", + "ORM/DBAL": "ORM/DBAL", + "ORM MySQL": "ORM MySQL", + "ORM Postgres": "ORM Postgres", + "ORM SQLite": "ORM SQLite", + "ORM Mongo": "ORM Mongo", + "Fundamentals": "基础", + "Event": "事件", + "Template": "模板", + "Workflow": "工作流", + "Stopwatch": "秒表", + "Tools": "工具", + "Devtool": "开发工具", + "BSON": "BSON", + "Runtime": "运行时", + "Bun": "Bun", + "Topsort": "拓扑排序", + "Type Compiler": "类型编译器", + "Type": "类型", + "API console": "API 控制台", + "Bench": "基准", + "Core": "核心", + "Vite": "Vite", + "Deepkit is a modular framework for TypeScript backend web applications.": "Deepkit 是一个用于 TypeScript 后端 web 应用程序的模块化框架。", + "Structured, scalable, and built for enterprise-grade architecture.": "结构化、可扩展,并为企业级架构而构建。", + "Getting Started": "入门", + "View on GitHub": "在 GitHub 上查看", + "Command line interface (CLI) parser, config loader, dependency injection container, event system, modules system.": "命令行界面 (CLI) 解析器、配置加载器、依赖注入容器、事件系统、模块系统。", + "App module that provides application/HTTP/RPC server, worker, debugger, integration tests.": "提供应用程序/HTTP/RPC 服务器、工作器、调试器、集成测试的应用模块。", + "App module that provides HTTP server based on Node http module with validation and serialization.": "基于 Node http 模块并具备验证和序列化功能的 HTTP 服务器应用模块。", + "App module to integrate Angular SSR.": "用于集成 Angular SSR 的应用模块。", + "Remote procedure call (RPC) with binary encoding for WebSockets and TCP.": "用于 WebSockets 和 TCP 的二进制编码远程过程调用 (RPC)。", + "TCP server and client for Deepkit RPC.": "用于 Deepkit RPC 的 TCP 服务器和客户端。", + "Message broker with queues, pub/sub, key-value, 2 level cache, and distributed locks.": "具有队列、发布/订阅、键值、二级缓存和分布式锁的消息代理。", + "Broker Redis adapter.": "代理 Redis 适配器。", + "Unified API to work with local and remote filesystems.": "用于处理本地和远程文件系统的统一 API。", + "Fileystem FTP adapter.": "文件系统 FTP 适配器。", + "Fileystem SFTP (SSH) adapter.": "文件系统 SFTP (SSH) 适配器。", + "Fileystem S3 adapter.": "文件系统 S3 适配器。", + "Fileystem Google Storage adapter.": "文件系统 Google 存储适配器。", + "Fileystem adapter for Deepkit ORM.": "用于 Deepkit ORM 的文件系统适配器。", + "Object-relational Mapper (ORM) and data access library (DAL). MongoDB, SQLite, Postgres, MySQL.": "对象关系映射器 (ORM) 和数据访问库 (DAL)。MongoDB、SQLite、Postgres、MySQL。", + "MySQL Adapter for Deepkit ORM.": "Deepkit ORM 的 MySQL 适配器。", + "PostgreSQL Adapter for Deepkit ORM.": "Deepkit ORM 的 PostgreSQL 适配器。", + "SQLite Adapter for Deepkit ORM.": "Deepkit ORM 的 SQLite 适配器。", + "MongoDB Adapter for Deepkit ORM.": "Deepkit ORM 的 MongoDB 适配器。", + "Runtime types with reflection, JSON serialization, validation, and type guards.": "具有反射、JSON 序列化、验证和类型守卫的运行时类型。", + "Async and synchronous event dispatcher.": "异步和同步事件调度器。", + "Dependency injection (DI) container with modules, config, scopes, and nominal type alias/interface support.": "带有模块、配置、作用域和名义类型别名/接口支持的依赖注入 (DI) 容器。", + "HTML template engine based on JSX.": "基于 JSX 的 HTML 模板引擎。", + "Logger with scopes, colors, and custom transporter and formatter.": "具有作用域、颜色、定制传输器和格式化器的日志记录器。", + "Workflow engine / finite state machine.": "工作流引擎/有限状态机。", + "Profile and collect execution time of code.": "分析和收集代码的执行时间。", + "Chrome devtools for Deepkit (RPC).": "用于 Deepkit (RPC) 的 Chrome 开发工具。", + "Angular Desktop UI library.": "Angular 桌面 UI 库。", + "Web user interface to manage ORM data.": "用于管理 ORM 数据的 Web 用户界面。", + "Tools to run benchmarks and collect statistics.": "用于运行基准测试和收集统计数据的工具。", + "BSON encoder and decoder.": "BSON 编码器和解码器。", + "Core functions for working with JavaScript.": "用于处理 JavaScript 的核心功能。", + "Topological sorting algorithm.": "拓扑排序算法。", + "Vite plugin to use Deepkit runtime types.": "用于使用 Deepkit 运行时类型的 Vite 插件。", + "Bun plugin to use Deepkit runtime types.": "用于使用 Deepkit 运行时类型的 Bun 插件。", + "Type compiler as TypeScript plugin to make runtime types available.": "作为 TypeScript 插件的类型编译器,用于提供运行时类型。" +} diff --git a/website/src/translations/zh/documentation/app.md b/website/src/translations/zh/documentation/app.md new file mode 100644 index 000000000..5e25b52ce --- /dev/null +++ b/website/src/translations/zh/documentation/app.md @@ -0,0 +1,171 @@ +# Deepkit App + +Deepkit App 抽象是 Deepkit 应用的最基本构建块。如果你不单独使用这些库,通常从这里开始构建你的应用。它是一个普通的 TypeScript 文件,你用 Node.js 之类的运行时来执行。它是应用的入口,并提供一种方式来定义 CLI 命令、服务、配置、事件等。 + +命令行接口(CLI)程序通过终端以文本输入/输出的形式进行交互。这种与应用交互的方式的优势在于,只需要有一个终端即可,无论是本地还是通过 SSH 连接。 + +它提供: + +- CLI 命令 +- 模块系统 +- 服务容器 +- 依赖注入 +- 事件系统 +- 日志记录器 +- 配置加载器(env、dotenv、json) + +Deepkit 中的命令可以完全访问 DI 容器,从而访问所有提供者和配置选项。CLI 命令的参数和选项通过 TypeScript 类型的参数声明进行控制,并会自动序列化和验证。 + +使用 `@deepkit/framework` 的 [Deepkit 框架](./framework.md) 进一步扩展了这些能力,提供用于 HTTP/RPC 的应用服务器、调试器/性能分析器等。 + +## 简易安装 + +最简单的开始方式是使用 NPM init 创建一个新的 Deepkit 项目。 + +```shell +npm init @deepkit/app@latest my-deepkit-app +```` + +这会创建一个包含所有依赖和基础 `app.ts` 文件的 `my-deepkit-app` 文件夹。 + +```sh +cd my-deepkit-app +npm run app +```` + +这将使用 `ts-node` 运行 `app.ts` 文件,并显示可用的命令。你可以从这里开始,添加自己的命令、控制器等。 + +## 手动安装 + +Deepkit App 基于 [Deepkit 运行时类型](./runtime-types.md),因此让我们安装所有依赖: + +```bash +mkdir my-project && cd my-project + +npm install typescript ts-node +npm install @deepkit/app @deepkit/type @deepkit/type-compiler +``` + +接下来,通过运行以下命令,确保 Deepkit 的类型编译器安装到 `node_modules/typescript` 中已安装的 TypeScript 包里: + +```sh +./node_modules/.bin/deepkit-type-install +``` + +确保所有 peer 依赖都已安装。默认情况下,NPM 7+ 会自动安装它们。 + +要编译你的应用,我们需要 TypeScript 编译器,并推荐使用 `ts-node` 来轻松运行应用。 + +使用 `ts-node` 的替代方案是使用 TypeScript 编译器编译源代码,并直接执行生成的 JavaScript 源代码。这样做的优势是在短命令下能显著提升执行速度。但这也会带来额外的工作流开销,例如需要手动运行编译器或设置监视器。因此,本手册的所有示例都使用 `ts-node`。 + +## 第一个应用 + +由于 Deepkit 框架不使用配置文件或特殊的文件夹结构,你可以按自己喜欢的方式组织项目。开始所需的仅有两个文件:TypeScript 的 app.ts 文件和 TypeScript 配置文件 tsconfig.json。 + +我们的目标是在项目文件夹中拥有以下文件: + +``` +. +├── app.ts +├── node_modules +├── package-lock.json +└── tsconfig.json +``` + +我们设置一个基础的 tsconfig 文件,并通过将 `reflection` 设为 `true` 来启用 Deepkit 的类型编译器。要使用依赖注入容器和其他特性,这是必需的。 + +```json title=tsconfig.json +{ + "compilerOptions": { + "outDir": "./dist", + "experimentalDecorators": true, + "strict": true, + "esModuleInterop": true, + "target": "es2020", + "module": "CommonJS", + "moduleResolution": "node" + }, + "reflection": true, + "files": [ + "app.ts" + ] +} +``` + +```typescript title=app.ts +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +const app = new App(); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +在这段代码中,你可以看到我们定义了一个 test 命令,并创建了一个新的应用,通过 `run()` 直接运行。运行该脚本即可启动应用。 + +接着直接运行它。 + +```sh +$ ./node_modules/.bin/ts-node app.ts +VERSION + Node + +USAGE + $ ts-node app.ts [COMMAND] + +TOPICS + debug + migration Executes pending migration files. Use migration:pending to see which are pending. + server Starts the HTTP server + +COMMANDS + test +``` + +现在,要执行我们的 test 命令,运行以下命令。 + +```sh +$ ./node_modules/.bin/ts-node app.ts test +Hello World +``` + +在 Deepkit 中,一切现在都通过这个 `app.ts` 完成。你可以按需重命名该文件或创建更多文件。自定义 CLI 命令、HTTP/RPC 服务器、迁移命令等都从这个入口启动。 + +## 参数与标志 + +Deepkit App 会自动将函数参数转换为 CLI 参数和标志。参数的顺序决定了 CLI 参数的顺序 + +参数可以是任意 TypeScript 类型,并会自动验证与反序列化。 + +更多信息参见章节 [参数与标志](./app/arguments.md)。 + +## 依赖注入 + +Deepkit App 会设置一个服务容器,并为每个导入的模块提供其自身的依赖注入容器,该容器从其父级继承。它开箱即用地提供以下提供者,你可以自动注入到服务、控制器和事件监听器中: + +- `Logger` 用于日志记录 +- `EventDispatcher` 用于事件处理 +- `CliControllerRegistry` 用于已注册的 CLI 命令 +- `MiddlewareRegistry` 用于已注册的中间件 +- `InjectorContext` 用于当前注入器上下文 + +一旦导入 Deepkit 框架,你将获得更多提供者。详见 [Deepkit 框架](./framework.md)。 + +## 退出码 + +默认情况下退出码为 0,这表示命令执行成功。要更改退出码,应在 execute 方法或命令回调中返回非 0 的数字。 + +```typescript + +@cli.controller('test') +export class TestCommand { + async execute() { + console.error('Error :('); + return 12; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/app/arguments.md b/website/src/translations/zh/documentation/app/arguments.md new file mode 100644 index 000000000..b363fd924 --- /dev/null +++ b/website/src/translations/zh/documentation/app/arguments.md @@ -0,0 +1,315 @@ +# 参数与标志 + +命令行中的命令参数就是 `execute` 方法或函数的常规参数。它们会被自动映射到命令行参数。 +如果你将某个参数标记为可选,则不必传入它。如果你为其设置了默认值,也同样不必传入。 + +根据类型(string、number、联合类型等),传入的值会被自动反序列化并验证。 + +```typescript +import { cli } from '@deepkit/app'; + +// 函数式 +new App().command('test', (name: string) => { + console.log('Hello', name); +}); + +// 类 +@cli.controller('test') +class TestCommand { + async execute(name: string) { + console.log('Hello', name); + } +} +``` + +如果现在执行该命令但未指定 name 参数,将会报告错误: + +```sh +$ ts-node app.ts test +RequiredArgsError: Missing 1 required arg: +name +``` + +使用 `--help` 可以获得关于必需参数的更多信息: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node-script app.ts test NAME +``` + +一旦将 name 作为参数传入,命令会被执行,并且该名称会被正确传递。 + +```sh +$ ts-node app.ts test "beautiful world" +Hello beautiful world +``` + +所有原始参数类型,如 string、number、boolean、字符串字面量、它们的联合类型,以及它们的数组,都会自动用作 CLI 参数,并自动进行验证与反序列化。参数的顺序决定了 CLI 参数的顺序。你可以添加任意数量的参数。 + +一旦定义了复杂对象(接口、类、对象字面量),它就会被视为服务依赖,依赖注入容器会尝试解析它。更多信息参见章节《[依赖注入](dependency-injection.md)》。 + +## 标志(Flags) + +标志是向命令传递值的另一种方式。大多数情况下它们是可选的,但也可以是必需的。使用 `Flag` 类型修饰的参数可以通过 `--name value` 或 `--name=value` 传入。 + +```typescript +import { Flag } from '@deepkit/app'; + +// 函数式 +new App().command('test', (id: number & Flag) => { + console.log('id', name); +}); + +// 类 +class TestCommand { + async execute(id: number & Flag) { + console.log('id', id); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + --id=id (required) +``` + +在帮助信息的“OPTIONS”中,你可以看到需要一个 `--id` 标志。若正确传入该标志,命令会接收到该值。 + +```sh +$ ts-node app.ts test --id 23 +id 23 + +$ ts-node app.ts test --id=23 +id 23 +``` + +### 布尔标志 + +标志的优势在于它也可以作为无值标志使用,例如用于激活某种行为。只要将参数标记为可选的布尔值,就会启用这种行为。 + +```typescript +import { Flag } from '@deepkit/app'; + +// 函数式 +new App().command('test', (remove: boolean & Flag = false) => { + console.log('delete?', remove); +}); + +// 类 +class TestCommand { + async execute(remove: boolean & Flag = false) { + console.log('delete?', remove); + } +} +``` + +```sh +$ ts-node app.ts test +delete? false + +$ ts-node app.ts test --remove +delete? true +``` + +### 多值标志 + +若要为同一个标志传入多个值,可以将标志标记为数组。 + +```typescript +import { Flag } from '@deepkit/app'; + +// 函数式 +new App().command('test', (id: number[] & Flag = []) => { + console.log('ids', id); +}); + +// 类 +class TestCommand { + async execute(id: number[] & Flag = []) { + console.log('ids', id); + } +} +``` + +```sh +$ ts-node app.ts test +ids: [] + +$ ts-node app.ts test --id 12 +ids: [12] + +$ ts-node app.ts test --id 12 --id 23 +ids: [12, 23] +``` + +### 单字符标志 + +要允许以单个字符传递标志,可以使用 `Flag<{char: 'x'}>`。 + +```typescript +import { Flag } from '@deepkit/app'; + +// 函数式 +new App().command('test', (output: string & Flag<{char: 'o'}>) => { + console.log('output: ', output); +}); + +// 类 +class TestCommand { + async execute(output: string & Flag<{char: 'o'}>) { + console.log('output: ', output); + } +} +``` + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test + +OPTIONS + -o, --output=output (required) + + +$ ts-node app.ts test --output test.txt +output: test.txt + +$ ts-node app.ts test -o test.txt +output: test.txt +``` + +## 可选 / 默认值 + +方法/函数的签名决定了哪些参数或标志是可选的。如果在类型系统中参数是可选的,用户就不必提供它。 + +```typescript + +// 函数式 +new App().command('test', (name?: string) => { + console.log('Hello', name || 'nobody'); +}); + +// 类 +class TestCommand { + async execute(name?: string) { + console.log('Hello', name || 'nobody'); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +对具有默认值的参数也是如此: + +```typescript +// 函数式 +new App().command('test', (name: string = 'body') => { + console.log('Hello', name); +}); + +// 类 +class TestCommand { + async execute(name: string = 'body') { + console.log('Hello', name); + } +} +``` + +```sh +$ ts-node app.ts test +Hello nobody +``` + +标志也以同样的方式适用。 + +## 序列化 / 验证 + +所有参数与标志都会基于其类型自动反序列化、验证,并且可以附加额外的约束。 + +因此,虽然命令行界面基于文本(字符串),但在控制器中被定义为 number 的参数总能保证是数字类型。 + +```typescript +// 函数式 +new App().command('test', (id: number) => { + console.log('id', id, typeof id); +}); + +// 类 +class TestCommand { + async execute(id: number) { + console.log('id', id, typeof id); + } +} +``` + +```sh +$ ts-node app.ts test 123 +id 123 number +``` + +可以使用来自 `@deepkit/type` 的类型注解定义额外的约束。 + +```typescript +import { Positive } from '@deepkit/type'; +// 函数式 +new App().command('test', (id: number & Positive) => { + console.log('id', id, typeof id); +}); + +// 类 +class TestCommand { + async execute(id: number & Positive) { + console.log('id', id, typeof id); + } +} +``` + +`id` 中的 `Postive` 类型表示只允许正数。如果用户现在传入负数,代码将不会被执行,并显示错误信息。 + +```sh +$ ts-node app.ts test -123 +Validation error in id: Number needs to be positive [positive] +``` + +这种非常容易实现的额外验证,使命令对错误输入更加稳健。更多信息参见章节《[验证](../runtime-types/validation.md)》。 + +## 描述 + +要描述一个标志或参数,请使用 `@description` 注释装饰器。 + +```typescript +import { Positive } from '@deepkit/type'; + +class TestCommand { + async execute( + /** @description 用户的标识符 */ + id: number & Positive, + /** @description 删除该用户? */ + remove: boolean = false + ) { + console.log('id', id, typeof id); + } +} +``` + +在帮助信息中,这些描述会显示在标志或参数之后: + +```sh +$ ts-node app.ts test --help +USAGE + $ ts-node app.ts test ID + +ARGUMENTS + ID The users identifier + +OPTIONS + --remove Delete the user? +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/app/configuration.md b/website/src/translations/zh/documentation/app/configuration.md new file mode 100644 index 000000000..9877daa06 --- /dev/null +++ b/website/src/translations/zh/documentation/app/configuration.md @@ -0,0 +1,265 @@ +# 配置 + +在 Deepkit 应用中,模块和你的应用本身都可以拥有配置选项。 +例如,配置可以包含数据库 URL、密码、IP 等。服务、HTTP/RPC/CLI 控制器以及模板函数可以通过依赖注入读取这些配置选项。 + +可以通过定义带有属性的类来定义配置。这是一种为整个应用定义配置的类型安全方式,其值会自动序列化并验证。 + +## 示例 + +```typescript +import { MinLength } from '@deepkit/type'; +import { App } from '@deepkit/app'; + +class Config { + pageTitle: string & MinLength<2> = 'Cool site'; + domain: string = 'example.com'; + debug: boolean = false; +} + +const app = new App({ + config: Config +}); + + +app.command('print-config', (config: Config) => { + console.log('config', config); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Hello from Cool site via example.com +``` + +当未使用任何配置加载器时,将使用默认值。要更改配置,你可以使用 `app.configure({domain: 'localhost'})` 方法,或使用环境配置加载器。 + +## 设置配置值 + +默认情况下,不会覆盖任何值,因此会使用默认值。设置配置值有多种方式。 + +* 通过 `app.configure({})` +* 为每个选项使用环境变量 +* 通过 JSON 的环境变量 +* dotenv 文件 + +你可以同时使用多种方法来加载配置。它们被调用的顺序很重要。 + +### 环境变量 + +要允许通过各自的环境变量设置每个配置选项,请使用 `loadConfigFromEnv`。默认前缀为 `APP_`,你可以更改它。它还会自动加载 `.env` 文件。默认使用大写命名策略,你也可以更改。 + +对于像上面的 `pageTitle` 这样的配置选项,你可以使用 `APP_PAGE_TITLE="Other Title"` 来更改其值。 + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({prefix: 'APP_'}) + .run(); +``` + +```sh +APP_PAGE_TITLE="Other title" ts-node app.ts server:start +``` + +### JSON 环境变量 + +要通过单个环境变量更改多个配置选项,请使用 `loadConfigFromEnvVariable`。第一个参数是环境变量的名称。 + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnvVariable('APP_CONFIG') + .run(); +``` + +```sh +APP_CONFIG='{"pageTitle": "Other title"}' ts-node app.ts server:start +``` + +### DotEnv 文件 + +要通过 dotenv 文件更改多个配置选项,请使用 `loadConfigFromEnv`。第一个参数可以是一个 dotenv 文件的路径(相对于 `cwd`),也可以是多个路径。如果是数组,将按顺序尝试每个路径,直到找到存在的文件为止。 + +```typescript +new App({ + config: config, + controllers: [MyWebsite], +}) + .loadConfigFromEnv({envFilePath: ['production.dotenv', 'dotenv']}) + .run(); +``` + +```sh +$ cat dotenv +APP_PAGE_TITLE=Other title +$ ts-node app.ts server:start +``` + +### 模块配置 + +每个被导入的模块都可以有一个模块名。该名称用于上面提到的配置路径。 + +例如,对于环境变量配置,`FrameworkModule` 的端口选项对应的路径是 `FRAMEWORK_PORT`。默认情况下,所有名称都写成大写。如果使用了 `APP_` 前缀,则可以通过如下方式更改端口: + +```sh +$ APP_FRAMEWORK_PORT=9999 ts-node app.ts server:start +2021-06-12T18:59:26.363Z [LOG] Start HTTP server, using 1 workers. +2021-06-12T18:59:26.365Z [LOG] HTTP MyWebsite +2021-06-12T18:59:26.366Z [LOG] GET / helloWorld +2021-06-12T18:59:26.366Z [LOG] HTTP listening at http://localhost:9999/ +``` + +在 dotenv 文件中也同样写作 `APP_FRAMEWORK_PORT=9999`。 + +而在通过 `loadConfigFromEnvVariable('APP_CONFIG')` 的 JSON 环境变量中,使用的是实际配置类的结构。`framework` 变成一个对象。 + +```sh +$ APP_CONFIG='{"framework": {"port": 9999}}' ts-node app.ts server:start +``` + +这对所有模块都同样适用。对于你的应用自身的配置选项(`new App`)不需要模块前缀。 + + +## 配置类 + +```typescript +import { MinLength } from '@deepkit/type'; + +export class Config { + title!: string & MinLength<2>; //这使其成为必填项,必须提供 + host?: string; + + debug: boolean = false; //也支持默认值 +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} +``` + +配置选项的值可以在模块的构造函数中提供,使用 `.configure()` 方法提供,或通过配置加载器提供(例如环境变量加载器)。 + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [new MyModule({title: 'Hello World'})], +}).run(); +``` + +要动态更改已导入模块的配置选项,可以使用 `process` 钩子。这是一个很好的位置,用于重定向配置选项,或根据当前模块配置或其他模块实例信息来设置已导入模块。 + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} +``` + +在应用层级,这稍有不同: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +当根应用模块由常规模块创建时,其工作方式与常规模块类似。 + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## 读取配置值 + +在服务中使用配置选项时,你可以使用常规的依赖注入。可以注入整个配置对象、单个值或配置的一部分。 + +### 部分 + +若只注入配置值的子集,请使用 `Pick` 类型。 + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private config: Pick<Config, 'title' | 'host'}) { + } + + getTitle() { + return this.config.title; + } +} + + +//在单元测试中,可以通过以下方式实例化 +new MyService({title: 'Hello', host: '0.0.0.0'}); + +//或者你可以使用类型别名 +type MyServiceConfig = Pick<Config, 'title' | 'host'}; +export class MyService { + constructor(private config: MyServiceConfig) { + } +} +``` + +### 单个值 + +若只注入单个值,请使用索引访问操作符。 + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private title: Config['title']) { + } + + getTitle() { + return this.title; + } +} +``` + +### 全部 + +若要注入所有配置值,请使用该类作为依赖。 + +```typescript +import { Config } from './module.config'; + +export class MyService { + constructor(private config: Config) { + } + + getTitle() { + return this.config.title; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/app/dependency-injection.md b/website/src/translations/zh/documentation/app/dependency-injection.md new file mode 100644 index 000000000..b851d0230 --- /dev/null +++ b/website/src/translations/zh/documentation/app/dependency-injection.md @@ -0,0 +1,38 @@ +# 依赖注入 + +所有命令都可以完全访问依赖注入容器。你可以在命令或控制器的构造函数中定义依赖项, +并且依赖注入容器会尝试解析它们。 + +更多信息请参阅[依赖注入](../dependency-injection.md)一章。 + +```typescript +import { App, cli } from '@deepkit/app'; +import { Logger, ConsoleTransport } from '@deepkit/logger'; + +new App({ + providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport])}], +}).command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); +``` + +```typescript +@cli.controller('test', { + description: 'My super first command' +}) +class TestCommand { + constructor(protected logger: Logger) { + } + + async execute() { + this.logger.log('Hello World!'); + } +} + +new App({ + providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport]}], + controllers: [TestCommand] +}).run(); +``` + +你可以根据需要定义任意多的依赖项。依赖注入容器会自动解析它们。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/app/events.md b/website/src/translations/zh/documentation/app/events.md new file mode 100644 index 000000000..f4def480c --- /dev/null +++ b/website/src/translations/zh/documentation/app/events.md @@ -0,0 +1,248 @@ +# 事件系统 + +事件系统允许同一进程内的应用组件通过发送和监听事件进行通信。这通过促进可能彼此并不直接知晓的函数之间的消息交换来帮助代码模块化。 + +应用程序或库在其运行过程中的特定时刻提供执行额外函数的机会。这些额外函数将自己注册为“事件监听器”。 + +事件可以有多种形式: + +- 应用程序启动或关闭。 +- 新用户被创建或删除。 +- 抛出错误。 +- 收到新的 HTTP 请求。 + +Deepkit Framework 及其相关库提供了一系列事件,用户可以监听并作出响应。此外,用户也可以灵活地创建任意数量的自定义事件,从而以模块化方式扩展应用。 + +## 用法 + +如果你使用 Deepkit 应用,事件系统已包含且可直接使用。 + +```typescript +import { App, onAppExecute } from '@deepkit/app'; + +const app = new App(); + +app.listen(onAppExecute, async (event) => { + console.log('MyEvent triggered!'); +}); + +app.run(); +``` + +可以使用 `listen()` 方法注册事件,或使用带有 `@eventDispatcher.listen` 装饰器的类注册: + +```typescript +import { App, onAppExecute } from '@deepkit/app'; +import { eventDispatcher } from '@deepkit/event'; + +class MyListener { + @eventDispatcher.listen(onAppExecute) + onMyEvent(event: typeof onAppExecute.event) { + console.log('MyEvent triggered!'); + } +} + +const app = new App({ + listeners: [MyListener], +}); +app.run(); +``` + +## 事件令牌 + +Deepkit 事件系统的核心是事件令牌(Event Token)。它们是指定事件 ID 和事件类型的唯一对象。事件令牌有两个主要用途: + +- 作为事件的触发器。 +- 监听它所触发的事件。 + +当使用事件令牌启动事件时,该令牌的所有者被有效地视为事件的来源。令牌决定与事件关联的数据,并指定是否可以使用异步事件监听器。 + +```typescript +import { EventToken } from '@deepkit/event'; + +const MyEvent = new EventToken('my-event'); + +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); + +//trigger via app reference +await app.dispatch(MyEvent); + +//or use the EventDispatcher, App's DI container injects it automatically +app.command('test', async (dispatcher: EventDispatcher) => { + await dispatcher.dispatch(MyEvent); +}); +``` + +### 创建自定义事件数据: + +使用来自 @deepkit/event 的 `DataEventToken`: + +```typescript +import { DataEventToken } from '@deepkit/event'; + +class User { +} + +const MyEvent = new DataEventToken<User>('my-event'); +``` + +扩展 BaseEvent: + +```typescript +class MyEvent extends BaseEvent { + user: User = new User; +} + +const MyEventToken = new EventToken<MyEvent>('my-event'); +``` + +## 函数式监听器 + +函数式监听器允许用户直接向调度器注册一个简单的函数回调。如下所示: + +```typescript +app.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +``` + +如果你希望引入额外的参数,比如 `logger: Logger`,它们会由于 Deepkit 的运行时类型反射而由依赖注入系统自动注入。 + +```typescript +app.listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); +}); +``` + +请注意,第一个参数必须是事件本身。你不能省略这个参数。 + +如果你使用 `@deepkit/app`,也可以使用 app.listen() 来注册函数式监听器。 + +```typescript +import { App } from '@deepkit/app'; + +new App() + .listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## 基于类的监听器 + +类监听器是带有装饰器的类。它们为监听事件提供一种结构化的方式。 + +```typescript +import { App } from '@deepkit/app'; + +class MyListener { + @eventDispatcher.listen(UserAdded) + onUserAdded(event: typeof UserAdded.event) { + console.log('User added!', event.user.username); + } +} + +new App({ + listeners: [MyListener], +}).run(); +``` + +对于类监听器,依赖注入可通过方法参数或构造函数来实现。 + +## 依赖注入 + +Deepkit 的事件系统拥有强大的依赖注入机制。在使用函数式监听器时,额外的参数会由于运行时类型反射系统而被自动注入。同样,基于类的监听器也支持通过构造函数或方法参数进行依赖注入。 + +例如,在函数式监听器的情况下,如果你添加一个类似 `logger: Logger` 的参数,当函数被调用时会自动提供正确的 Logger 实例。 + +```typescript +import { App } from '@deepkit/app'; +import { Logger } from '@deepkit/logger'; + +new App() + .listen(MyEvent, (event, logger: Logger) => { + console.log('MyEvent triggered!'); + }) + .run(); +``` + +## 事件传播 + +每个事件对象都配备了一个 stop() 函数,允许你控制事件的传播。如果事件被停止,则不会执行之后注册(按添加顺序)的监听器。这为事件的执行和处理提供了精细的控制,特别适用于某些条件可能需要停止事件处理的场景。 + +例如: + +```typescript +dispatcher.listen(MyEventToken, (event) => { + if (someCondition) { + event.stop(); + } + // Further processing +}); +``` + +借助 Deepkit 框架的事件系统,开发者可以轻松创建模块化、可扩展且可维护的应用程序。理解事件系统能够让你根据特定的发生情况或条件来灵活定制应用程序行为。 + +## 框架事件 + +Deepkit Framework 本身具有来自应用服务器的多个事件,你可以监听这些事件。 + +_函数式监听器_ + +```typescript +import { onServerMainBootstrap } from '@deepkit/framework'; +import { onAppExecute } from '@deepkit/app'; + +new App({ + imports: [new FrameworkModule] +}) + .listen(onAppExecute, (event) => { + console.log('Command about to execute'); + }) + .listen(onServerMainBootstrap, (event) => { + console.log('Server started'); + }) + .run(); +``` + +| 名称 | 描述 | +|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| onServerBootstrap | 仅在应用服务器引导时调用一次(主进程和工作进程)。 | +| onServerBootstrapDone | 仅在应用服务器引导时调用一次(主进程和工作进程),一旦应用服务器已启动。 | +| onServerMainBootstrap | 仅在应用服务器引导时调用一次(在主进程)。 | +| onServerMainBootstrapDone | 仅在应用服务器引导时调用一次(在主进程),一旦应用服务器已启动。 | +| onServerWorkerBootstrap | 仅在应用服务器引导时调用一次(在工作进程)。 | +| onServerWorkerBootstrapDone | 仅在应用服务器引导时调用一次(在工作进程),一旦应用服务器已启动。 | +| onServerShutdownEvent | 当应用服务器关闭时调用(在主进程和每个工作进程)。 | +| onServerMainShutdown | 当应用服务器在主进程关闭时调用。 | +| onServerWorkerShutdown | 当应用服务器在工作进程关闭时调用。 | +| onAppExecute | 即将执行命令时。 | +| onAppExecuted | 当命令成功执行时。 | +| onAppError | 当命令执行失败时。 | +| onAppShutdown | 当应用程序即将关闭时。 | + +## 底层 API + +下面是来自 @deepkit/event 的底层 API 示例。使用 Deepkit 应用时,事件监听器不是直接通过 EventDispatcher 注册,而是通过模块注册。但如果你愿意,仍然可以使用底层 API。 + +```typescript +import { EventDispatcher, EventToken } from '@deepkit/event'; + +//first argument can be a injector context to resolve dependencies for dependency injection +const dispatcher = new EventDispatcher(); +const MyEvent = new EventToken('my-event'); + +dispatcher.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); +}); +dispatcher.dispatch(MyEvent); + +``` + +### 安装 + +事件系统已包含在 @deepkit/app 中。如果你想单独使用,可以手动安装: + +有关安装说明,请参见[事件](../event.md)。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/app/logger.md b/website/src/translations/zh/documentation/app/logger.md new file mode 100644 index 000000000..9894eae6a --- /dev/null +++ b/website/src/translations/zh/documentation/app/logger.md @@ -0,0 +1,130 @@ +# Logger + +Deepkit Logger 是一个独立的库,提供一个主要的 Logger 类用于记录日志信息。该类会自动在 Deepkit 应用的依赖注入容器中可用。 + +`Logger` 类提供了多个方法,它们的行为与 `console.log` 一致。 + +| 名称 | 日志级别 | 级别 ID | +|------------------|----------------------|---------| +| logger.error() | 错误 | 1 | +| logger.warning() | 警告 | 2 | +| logger.log() | 默认日志 | 3 | +| logger.info() | 特殊信息 | 4 | +| logger.debug() | 调试信息 | 5 | + + +默认情况下,logger 的级别是 `info`,也就是说它只处理 info 以及更高(即 log、warning、error,但不包括 debug)的消息。要更改日志级别,例如调用 `logger.level = 5`。 + +## 在应用中使用 + +要在 Deepkit 应用中使用 logger,只需将 `Logger` 注入到你的服务或控制器中。 + +```typescript +import { Logger } from '@deepkit/logger'; +import { App } from '@deepkit/app'; + +const app = new App(); +app.command('test', (logger: Logger) => { + logger.log('This is a <yellow>log message</yellow>'); +}); + +app.run(); +``` + +## 颜色 + +logger 支持彩色日志消息。你可以通过使用包裹目标文本的 XML 标签来提供颜色。 + +```typescript +const username = 'Peter'; +logger.log(`Hi <green>${username}</green>`); +``` + +对于不支持颜色的传输器,会自动移除颜色信息。在默认传输器(`ConsoleTransport`)中会显示颜色。可用的颜色包括:`black`、`red`、`green`、`blue`、`cyan`、`magenta`、`white` 和 `grey`/`gray`。 + +## 传输器 + +你可以配置单个传输器或多个传输器。在 Deepkit 应用中,会自动配置 `ConsoleTransport` 传输器。要配置额外的传输器,可以使用[设置调用](dependency-injection.md#di-setup-calls): + +```typescript +import { Logger, LoggerTransport } from '@deepkit/logger'; + +export class MyTransport implements LoggerTransport { + write(message: string, level: LoggerLevel, rawMessage: string) { + process.stdout.write(JSON.stringify({message: rawMessage, level, time: new Date}) + '\n'); + } + + supportsColor() { + return false; + } +} + +new App() + .setup((module, config) => { + module.configureProvider<Logger>(v => v.addTransport(new MyTransport)); + }) + .run(); +``` + +要用一组新的传输器替换所有传输器,请使用 `setTransport`: + +```typescript +import { Logger } from '@deepkit/logger'; + +new App() +.setup((module, config) => { + module.configureProvider<Logger>(v => v.setTransport([new MyTransport])); +}) +.run(); +``` + +```typescript +import { Logger, JSONTransport } from '@deepkit/logger'; + +new App() + .setup((module, config) => { + module.configureProvider<Logger>(v => v.setTransport([new JSONTransport])); + }) + .run(); +``` + +## 作用域 Logger + +作用域 logger 会为每条日志添加任意的区域名称,这有助于判断日志来自应用的哪个子区域。 + +```typescript +const scopedLogger = logger.scoped('database'); +scopedLogger.log('Query', query); +``` + +还提供了 `ScopedLogger` 类型,你可以将其注入到服务中来获取带作用域的 logger。 + +```typescript +import { ScopedLogger } from '@deepkit/logger'; + +class MyService { + constructor(protected logger: ScopedLogger) {} + doSomething() { + this.logger.log('This is wild'); + } +} +``` + +现在,来自作用域 logger 的所有消息都会带有 `MyService` 作用域名称前缀。 + +## 格式化器 + +通过格式化器,你可以更改消息格式,例如添加时间戳。当应用通过 `server:start` 启动时,如果没有其他可用的格式化器,会自动添加 `DefaultFormatter`(它会添加时间戳、范围和日志级别)。 + +## 上下文数据 + +要向日志条目添加上下文数据,只需将一个简单的对象字面量作为最后一个参数添加。只有至少两个参数的日志调用才可以包含上下文数据。 + +```typescript +const query = 'SELECT *'; +const user = new User; +logger.log('Query', {query, user}); //最后一个参数是上下文数据 +logger.log('Another', 'wild log entry', query, {user}); //最后一个参数是上下文数据 + +logger.log({query, user}); //这不会被当作上下文数据处理。 +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/app/modules.md b/website/src/translations/zh/documentation/app/modules.md new file mode 100644 index 000000000..e9d9bcbd8 --- /dev/null +++ b/website/src/translations/zh/documentation/app/modules.md @@ -0,0 +1,514 @@ +# 模块 + +Deepkit 高度模块化,允许你将应用拆分为多个实用的模块。每个模块都有自己的依赖注入子容器(继承所有父级提供者)、配置、命令等更多内容。 +在[入门](../framework.md)一章中,你已经创建了一个模块——根模块。`new App` 接受的参数与模块几乎相同,因为它会在后台为你自动创建根模块。 + +如果你不打算将应用拆分为子模块,或者不打算将模块作为包提供给他人,可以跳过本章。 + +模块可以定义为类模块或函数式模块。 + +```typescript title=类模块 +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + //与 new App({}) 的选项相同 + providers: [MyService] +}) { +} +``` + +```typescript title=函数式模块 +import { AppModule } from '@deepkit/app'; + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addProvider(MyService); + }; +} +``` + +然后可以在你的应用或其他模块中导入该模块。 + +```typescript +import { MyModule, myModule } from './module.ts' + +new App({ + imports: [ + new MyModule(), //导入类模块 + myModule(), //导入函数式模块 + ] +}).run(); +``` + +现在你可以像使用 `App` 一样为该模块添加功能。`createModule` 的参数是相同的,除了在模块定义中不可用 imports。 +对于函数式模块,你可以使用 `AppModule` 的方法来基于你自己的选项进行动态配置。 + +添加 HTTP/RPC/CLI 控制器、服务、配置、事件监听器,以及各种模块钩子,使模块更加动态。 + +## 控制器 + +模块可以定义由其他模块处理的控制器。举例来说,如果你添加了使用 `@deepkit/http` 包中的装饰器的控制器,其 `HttpModule` 将会拾取这些控制器并在其路由器中注册找到的路由。一个控制器可能包含多个这样的装饰器。如何处理这些控制器取决于为你提供这些装饰器的模块作者。 + +在 Deepkit 中,有三个处理此类控制器的包:HTTP、RPC 和 CLI。参阅它们各自的章节以了解更多。下面是一个 HTTP 控制器示例: + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +class MyHttpController { + @http.GET('/hello) + hello() { + return 'Hello world!'; + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] +}) { +} + + +//同样也可以用于 App +new App({ + controllers: [MyHttpController] +}).run(); +``` + +## 提供者 + +当你在应用的 `providers` 部分定义提供者时,它在整个应用中都可访问。而对于模块,这些提供者会自动封装在该模块的依赖注入子容器中。你必须手动导出每个提供者,才能使其对其他模块或你的应用可用。 + +要了解提供者如何工作,请参阅[依赖注入](../dependency-injection.md)一章。 + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { http } from '@deepkit/http'; +import { injectable } from '@deepkit/injector'; + +export class HelloWorldService { + helloWorld() { + return 'Hello there!'; + } +} + +class MyHttpController { + constructor(private helloService: HelloWorldService) { + } + + @http.GET('/hello) + hello() { + return this.helloService.helloWorld(); + } +} + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addController(MyHttpController); + module.addProvider(HelloWorldService); + }; +} + +//同样也可以用于 App +new App({ + controllers: [MyHttpController], + providers: [HelloWorldService], +}).run(); +``` + +当用户导入该模块时,他无法访问 `HelloWorldService`,因为它被封装在 `MyModule` 的子依赖注入容器中。 + +## 导出 + +为了让提供者在导入方模块中可用,你可以将提供者的令牌包含在 `exports` 中。这实际上是将提供者上移一级到父模块(导入者)的依赖注入容器中。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + exports: [HelloWorldService], +}) { +} + +export function myModule(options: {} = {}) { + return (module: AppModule) => { + module.addExport(HelloWorldService); + }; +} +``` + +如果你有其他提供者,如 `FactoryProvider`、`UseClassProvider` 等,你仍然只应在导出中使用类类型。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({ + controllers: [MyHttpController] + providers: [ + { provide: HelloWorldService, useValue: new HelloWorldService } + ], + exports: [HelloWorldService], +}) { +} +``` + +现在我们可以导入该模块并在我们的应用代码中使用其导出的服务。 + +```typescript +import { App } from '@deepkit/app'; +import { cli, Command } from '@deepkit/app'; +import { HelloWorldService, MyModule } from './my-module'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected helloWorld: HelloWorldService) { + } + + async execute() { + this.helloWorld.helloWorld(); + } +} + +new App({ + controllers: [TestCommand], + imports: [ + new MyModule(), + ] +}).run(); +``` + +阅读[依赖注入](../dependency-injection.md)一章了解更多信息。 + + +### 配置模式 + +模块可以拥有类型安全的配置选项。可以使用简单的类引用或类型函数(如 `Partial<Config, 'url'>`)将这些选项的值部分或全部注入到该模块的服务中。要定义配置模式,请编写一个带有属性的类。 + +```typescript +export class Config { + title!: string; //必填,必须提供 + host?: string; //可选 + + debug: boolean = false; //也支持默认值 +} +``` + +```typescript +import { createModuleClass } from '@deepkit/app'; +import { Config } from './module.config.ts'; + +export class MyModule extends createModuleClass({ + config: Config +}) { +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.setConfigDefinition(Config).configure(options); + }; +} +``` + +配置选项的值可以通过模块的构造函数、`.configure()` 方法或通过配置加载器(例如环境变量加载器)提供。 + +```typescript +import { MyModule } from './module.ts'; + +new App({ + imports: [ + new MyModule({title: 'Hello World'}), + myModule({title: 'Hello World'}), + ], +}).run(); +``` + +要动态更改已导入模块的配置选项,可以使用 `process` 模块钩子。这是一个很好的位置,可以重定向配置选项,或根据当前模块配置或其他模块实例信息来设置已导入的模块。 + +```typescript +import { MyModule } from './module.ts'; + +export class MainModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }; +} +``` + +在应用层级,工作方式略有不同: + +```typescript +new App({ + imports: [new MyModule({title: 'Hello World'}], +}) + .setup((module, config) => { + module.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + }) + .run(); +``` + +如果根应用模块是由常规模块创建的,其工作方式与常规模块类似。 + +```typescript +class AppModule extends createModuleClass({ +}) { + process() { + this.getImportedModuleByClass(MyModule).configure({title: 'Changed'}); + } +} + +App.fromModule(new AppModule()).run(); +``` + +## 模块名称 + +所有配置选项也可以通过环境变量进行更改。这仅在模块已分配名称时有效。模块名称可以通过 `createModule` 定义,并可在实例创建时动态更改。后一种模式在你两次导入相同模块并希望通过设置新名称来区分它们时非常有用。 + +```typescript +export class MyModule extends createModuleClass({ + name: 'my' +}) { +} + +export function myModule(options: Partial<Config> = {}) { + return (module: AppModule) => { + module.name = 'my'; + }; +} +``` + +```typescript +import { MyModule } from './module'; + +new App({ + imports: [ + new MyModule(), //'my' 是默认名称 + new MyModule().rename('my2'), //'my2' 现在是新名称 + ] +}).run(); +``` + +有关如何从环境变量或 .env 文件加载配置选项的更多信息,请参阅[配置](./configuration.md)一章。 + +## 导入 + +模块可以导入其他模块以扩展其功能。在 `App` 中,你可以通过模块定义对象中的 `imports: []` 导入其他模块: + +```typescript +new App({ + imports: [new Module] +}).run(); +``` + +在常规模块中,这不可行,因为对象定义中的模块实例将变成全局的,这通常不是你想要的。相反,可以在模块自身中通过 `imports` 属性实例化模块,这样每个导入的模块都会针对你的模块的每个新实例创建一个实例。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + imports = [new OtherModule()]; +} + +export function myModule() { + return (module: AppModule) => { + module.addImport(new OtherModule()); + }; +} +``` + +你还可以使用 `process` 钩子,基于配置动态导入模块。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + process() { + if (this.config.xEnabled) { + this.addImport(new OtherModule({ option: 'value' }); + } + } +} + +export function myModule(option: { xEnabled?: boolean } = {}) { + return (module: AppModule) => { + if (option.xEnabled) { + module.addImport(new OtherModule()); + } + }; +} +``` + +## 钩子 + +服务容器按照模块被导入的顺序加载所有模块,从根/应用模块开始。 + +在此过程中,服务容器还会执行所有已注册的配置加载器,调用 `setupConfig` 回调,然后验证每个模块的配置对象。 + +加载服务容器的整个流程如下: + +1. 对于每个模块 `T`(从根开始) + 1. 执行配置加载器 `ConfigLoader.load(T)`。 + 2. 调用 `T.setupConfig()`。 + 3. 验证 `T` 的配置。如无效则中止。 + 4. 调用 `T.process()`。 + 此处模块可以基于已验证的配置选项修改自身。添加新的导入、提供者等。 + 5. 对 `T` 的每个已导入模块重复 1。 +3. 查找所有已注册的模块。 +4. 处理找到的每个模块 `T`。 + 1. 注册 `T` 的中间件。 + 2. 在事件分发器中注册 `T` 的监听器。 + 3. 对步骤 2 中找到的所有模块调用 `Module.processController(T, controller)`。 + 4. 对步骤 2 中找到的所有模块调用 `Module.processProvider(T, token, provider)`。 + 5. 对 `T` 的每个已导入模块重复 3。 +5. 在所有模块上运行 `T.postProcess()`。 +6. 在所有模块上实例化引导类。 +7. 依赖注入容器现已构建完成。 + +要使用钩子,你可以在模块类中注册 `process`、`processProvider`、`postProcess` 方法。 + +```typescript +import { createModuleClass, AppModule } from '@deepkit/app'; +import { isClass } from '@deepkit/core'; +import { ProviderWithScope, Token } from '@deepkit/injector'; + +export class MyModule extends createModuleClass({}) { + imports = [new FrameworkModule()]; + + //最先执行 + process() { + //this.config 包含已完全验证的配置对象。 + if (this.config.environment === 'development') { + this.getImportedModuleByClass(FrameworkModule).configure({ debug: true }); + } + this.addModule(new AnotherModule); + this.addProvider(Service); + + //调用额外的设置方法。 + //在本例中,当依赖注入容器实例化 Service 时, + //以给定参数调用 'method1'。 + this.configureProvider<Service>(v => v.method1(this.config.value)); + } + + //为所有模块中找到的每个控制器执行 + processController(module: AppModule<any>, controller: ClassType) { + //例如 HttpModule 会检查每个控制器是否使用了 @http 装饰器, + //如果使用了,则提取所有路由信息并将其放入路由器。 + } + + //为所有模块中找到的每个提供者执行 + processProvider(module: AppModule<any>, token: Token, provider: ProviderWithScope) { + //例如 FrameworkModule 会查找扩展自 deepkit/orm Database 的提供令牌, + //并自动将它们注册到 DatabaseRegistry 中,以便它们可以用于迁移 CLI 命令 + //和 Framework 调试器。 + } + + //当所有模块都已处理完毕时执行。 + //基于在 process/processProvider 中处理的信息,最后一次通过 module.configureProvider 设置提供者的机会。 + postProcess() { + + } +} +``` + +## 有状态模块 + +由于每个模块都是通过 `new Module` 显式实例化的,该模块可以拥有状态。此状态可以被注入到依赖注入容器中,从而供服务使用。 + +例如,考虑 HttpModule 的用例。它检查整个应用中每个已注册的控制器是否具有某些 @http 装饰器,如果有,则将该控制器放入注册表。该注册表被注入到 Router 中,Router 在实例化后会提取这些控制器的所有路由信息并将它们注册。 + +```typescript +class Registry { + protected controllers: { module: AppModule<any>, classType: ClassType }[] = []; + + register(module: AppModule<any>, controller: ClassType) { + this.controllers.push({ module, classType: controller }); + } + + get(classType: ClassType) { + const controller = this.controllers.find(v => v.classType === classType); + if (!controller) throw new Error('Controller unknown'); + return controller; + } +} + +class Router { + constructor( + protected injectorContext: InjectorContext, + protected registry: Registry + ) { + } + + getController(classType: ClassType) { + //为给定的控制器 classType 查找 classType 和 module + const controller = this.registry.get(classType); + + //此处将实例化控制器。如果它已被实例化,并且提供者不是 transient: true, + //则会返回旧的实例 + return injector.get(controller.classType, controller.module); + } +} + +class HttpModule extends createModuleClass({ + providers: [Router], + exports: [Router], +}) { + protected registry = new Registry; + + process() { + this.addProvider({ provide: Registry, useValue: this.registry }); + } + + processController(module: AppModule<any>, controller: ClassType) { + //控制器需要由控制器消费者放入模块的 providers 中 + if (!module.isProvided(controller)) module.addProvider(controller); + this.registry.register(module, controller); + } +} + +class MyController {} + +const app = new App({ + controllers: [MyController], + imports: [new HttpModule()] +}); + +const myController = app.get(Router).getController(MyController); +``` + +## 关于 root + +`root` 属性允许你将模块的依赖注入容器移动到根应用的容器中。这样,模块中的每个服务都会自动在根应用中可用。它基本上将每个提供者(控制器、事件监听器、提供者)移动到根容器。这可能导致依赖冲突,因此仅应在模块确实只有全局内容时使用。相反,你应该更倾向于手动导出每个提供者。 + +如果你构建的是能被许多模块使用的库,你应避免使用 `root`,因为它可能与其他库的提供者令牌发生冲突。比如,如果该库模块导入了定义某个服务的 `foo` 模块,并且你按需重新配置了一些服务,而用户的应用也导入了相同的 `foo` 模块,那么用户将会接收到你重新配置过的服务。不过对于许多更简单的用例,这也许是可以接受的。 + +```typescript +import { createModuleClass } from '@deepkit/app'; + +export class MyModule extends createModuleClass({}) { + root = true; +} +``` + +你也可以通过使用 `forRoot()` 更改第三方模块的 `root` 属性。 + +```typescript +new App({ + imports: [new ThirdPartyModule().forRoot()], +}).run(); +``` + +## 注入器上下文 + +InjectorContext 是依赖注入容器。它允许你从你自己的模块或其他模块请求/实例化服务。例如,如果你在 `processControllers` 中存储了一个控制器,并希望正确地实例化它们,那么这是必要的。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/app/services.md b/website/src/translations/zh/documentation/app/services.md new file mode 100644 index 000000000..77bd35360 --- /dev/null +++ b/website/src/translations/zh/documentation/app/services.md @@ -0,0 +1,62 @@ +# 服务 + + +服务是一个广义的概念,涵盖应用所需的任何值、函数或特性。服务通常是一个具有狭窄且明确目的的类。它应当专注于做好一件具体的事情。 + +在 Deepkit(以及大多数其他 JavaScript/TypeScript 框架)中,服务是通过提供者在模块中注册的一个简单类。最简单的提供者是类提供者(class provider),它只指定类本身而不包含其他内容。随后,该类会在其定义所在模块的依赖注入容器中成为一个单例。 + +服务由依赖注入容器管理与实例化,因此可以通过构造函数注入或属性注入,在其他服务、控制器以及事件监听器中导入并使用。更多细节参见[依赖注入](../dependency-injection)一章。 + +要创建一个简单的服务,你只需编写一个有明确职责的类: + + +```typescript +export interface User { + username: string; +} + +export class UserManager { + users: User[] = []; + + addUser(user: User) { + this.users.push(user); + } +} +``` + +然后将其注册到你的应用或某个模块中: + +```typescript +new App({ + providers: [UserManager] +}).run(); +``` + +完成后,你就可以在控制器、其他服务或事件监听器中使用此服务。例如,我们在一个 CLI 命令或 HTTP 路由中使用它: + + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + providers: [UserManager], + imports: [new FrameworkModule({debug: true})] +}); + +app.command('test', (userManager: UserManager) => { + for (const user of userManager.users) { + console.log('User: ', user.username); + } +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', (userManager: UserManager) => { + return userManager.users; +}) + +app.run(); +``` + +服务是 Deepkit 的基础构建块,并不局限于类。实际上,服务可以是应用所需的任何值、函数或特性。想了解更多,请参阅[依赖注入](../dependency-injection)一章。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/broker.md b/website/src/translations/zh/documentation/broker.md new file mode 100644 index 000000000..0782b42b1 --- /dev/null +++ b/website/src/translations/zh/documentation/broker.md @@ -0,0 +1,156 @@ +# Deepkit Broker + +Deepkit Broker 是一个针对消息队列、消息总线、事件总线、发布/订阅、键值存储、缓存以及原子操作的高性能抽象。它以类型安全为核心,具备自动序列化与验证、高性能和可扩展性。 + +Deepkit Broker 同时是客户端和服务器。它可以作为独立服务器使用,或作为客户端连接到其他 Deepkit Broker 服务器。它被设计用于微服务架构,也可以用于单体应用。 + +客户端使用适配器模式以支持不同的后端。你可以用相同的代码在不同后端之间切换,甚至可以同时使用多个后端。 + +目前有 3 个可用适配器。一个是默认的 `BrokerDeepkitAdapter`,它与 Deepkit Broker 服务器通信,并随 Deepkit Broker 开箱即用(包括服务器)。 +第二个是位于 [@deepkit/broker-redis](./package/broker-redis) 的 `BrokerRedisAdapter`,它与 Redis 服务器通信。第三个是 `BrokerMemoryAdapter`,用于测试的内存适配器。 + +## 安装 + +使用 [Deepkit 框架](./framework.md) 时会默认安装并启用 Deepkit Broker。否则,可以通过以下方式安装: + +```bash +npm install @deepkit/broker +``` + +## Broker 类 + +Deepkit Broker 提供以下主要的 broker 类: + +- **BrokerCache** - 二级缓存(L2)抽象与缓存失效 +- **BrokerBus** - 消息总线(发布/订阅) +- **BrokerQueue** - 队列系统 +- **BrokerLock** - 分布式锁 +- **BrokerKeyValue** - 键值存储 + +这些类通过接收一个 broker 适配器,以类型安全的方式与 broker 服务器通信。 + +```typescript +import { BrokerBus, BrokerMemoryAdapter } from '@deepkit/broker'; + +const bus = new BrokerBus(new BrokerMemoryAdapter()); +const channel = bus.channel<{ foo: string }>('my-channel-name'); +await channel.subscribe((message) => { + console.log('received message', message); +}); + +await channel.publish({ foo: 'bar' }); +``` + +FrameworkModule 提供并注册默认适配器并连接到 Deepkit Broker 服务器,该服务器默认在同一进程中运行。 + +借助运行时类型以及调用 `channel<Type>('my-channel-name');`,一切都是类型安全的,消息可在适配器中直接自动验证与序列化。 +默认实现 `BrokerDeepkitAdapter` 会为你自动处理这些(并使用 BSON 进行序列化)。 + +请注意,每个 broker 类都有自己的适配器接口,因此你可以只实现所需的方法。`BrokerDeepkitAdapter` 实现了所有这些接口,可用于所有 broker API。 + +## 应用集成 + +要在使用依赖注入的 Deepkit 应用中使用这些类,可以使用 `FrameworkModule`,它提供以下内容: + +- broker 服务器的默认适配器 +- broker 服务器本身(并自动启动) +- 将所有 broker 类注册为提供者 + +`FrameworkModule` 会基于给定配置为已配置的 broker 服务器提供默认的 broker 适配器。 +它还会为所有 broker 类注册提供者,因此你可以将它们(例如 BrokerBus)直接注入到你的服务和提供者工厂中。 + +```typescript +// 在单独文件中,例如 broker-channels.ts +type MyBusChannel = BrokerBusChannel<MyMessage>; + +const app = new App({ + providers: [ + Service, + provide<MyBusChannel>((bus: BrokerBus) => bus.channel<MyMessage>('my-channel-name')), + ], + imports: [new FrameworkModule({ + broker: { + // 如果 startOnBootstrap 为 true,broker 服务器将启动在此地址。可以是 Unix 套接字路径或 host:port 组合 + listen: 'localhost:8811', // 或 'var/broker.sock'; + // 若要使用不同的 broker 服务器,这里是其地址。可以是 Unix 套接字路径或 host:port 组合。 + host: 'localhost:8811', //或 'var/broker.sock'; + // 在主进程中自动启动单个 broker。若有自定义 broker 节点,请禁用它。 + startOnBootstrap: true, + }, + })], +}); +``` + +然后你可以将派生的 broker 类(或直接使用 broker 类)注入到你的服务中: + +```typescript +import { MyBusChannel } from './broker-channels.ts'; + +class Service { + constructor(private bus: MyBusChannel) { + } + + async addUser() { + await this.bus.publish({ foo: 'bar' }); + } +} +``` + +为你的通道创建一个派生类型(如上所示的 `MyBusChannel`)通常是个好主意,这样你就可以轻松地将它们注入到服务中。 +否则,你每次需要时都必须注入 `BrokerBus` 并调用 `channel<MyMessage>('my-channel-name')`,这既容易出错,也不符合 DRY 原则。 + +几乎所有 broker 类都支持这种派生方式,你可以在一个地方轻松定义并在各处使用。有关更多信息,请参阅相应的 broker 类文档。 + +## 自定义适配器 + +如果你需要自定义适配器,可以通过实现 `@deepkit/broker` 中以下接口之一或多个来创建自己的适配器: + +```typescript +export type BrokerAdapter = BrokerAdapterCache & BrokerAdapterBus & BrokerAdapterLock & BrokerAdapterQueue & BrokerAdapterKeyValue; +``` + +```typescript +import { BrokerAdapterBus, BrokerBus, Release } from '@deepkit/broker'; +import { Type } from '@deepkit/type'; + +class MyAdapter implements BrokerAdapterBus { + disconnect(): Promise<void> { + // 实现:从 broker 服务器断开连接 + } + async publish(name: string, message: any, type: Type): Promise<void> { + // 实现:向 broker 服务器发送消息,name 为 'my-channel-name',message 为 { foo: 'bar' } + } + async subscribe(name: string, callback: (message: any) => void, type: Type): Promise<Release> { + // 实现:订阅 broker 服务器,name 为 'my-channel-name' + } +} + +// 或使用默认适配器 BrokerDeepkitAdapter +const adapter = new MyAdapter; +const bus = new BrokerBus(adapter); + +``` + +## Broker 服务器 + +默认情况下,一旦引入 `FrameworkModule` 并运行 `server:start` 命令,Broker 服务器就会自动启动。 +所有 Broker 类默认都配置为连接到该服务器。 + +在生产环境中,你通常会在单独的进程或不同的机器上运行 broker 服务器。 +你会使用 `server:broker:start` 来启动 broker 服务器,而不是 `server:start`。 + +如果你的流量很大并且需要扩展性,应该改用 [Redis 适配器](./package/broker-redis.md)。它的设置更复杂, +因为你需要运行一个 Redis 服务器,但性能更好,可以处理更多的流量。 + +```bash +ts-node app.ts server:broker:start +``` + +这会在(例如通过 `new FrameworkModule({broker: {listen: 'localhost:8811'}})`)配置的地址上启动服务器。 +如果启用了 `app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper'});`,你也可以通过环境变量修改地址: + +```bash +APP_FRAMEWORK_BROKER_LISTEN=localhost:8811 ts-node app.ts server:broker:start +``` + +如果你手动启动服务器,请确保在应用配置中通过 `startOnBootstrap: false` 禁用自动启动 broker 服务器。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/broker/atomic-locks.md b/website/src/translations/zh/documentation/broker/atomic-locks.md new file mode 100644 index 000000000..a3b51ef7d --- /dev/null +++ b/website/src/translations/zh/documentation/broker/atomic-locks.md @@ -0,0 +1,75 @@ +# Broker 原子锁 + +Deepkit Broker Locks 是一种在多个进程或机器之间创建原子锁的简单方法。 + +它提供一种简单方式,确保同一时间只有一个进程可以执行某个代码块。 + +## 用法 + +```typescript +import { BrokerLock } from '@deepkit/broker'; + +const lock = new BrokerLock(adapter); + +// 锁存活 60 秒。 +// 获取超时时间为 10 秒。 +const myLock = lock.item('my-lock', { ttl: '60s', timeout: '10s' }); + +async function criticalSection() { + // 在函数执行完之前持有该锁。 + // 即使函数抛出错误,也会自动清理该锁。 + await using hold = await lock1.hold(); +} +``` + +该锁支持[显式资源管理](https://github.com/tc39/proposal-explicit-resource-management),这意味着你无需使用 try-catch 块来确保锁被正确释放。 + +要手动获取和释放锁,可以使用 `acquire` 和 `release` 方法。 + +```typescript +// 在获取到锁之前会阻塞。 +// 若超过超时时间则抛出异常 +await myLock.acquire(); + +try { + // 执行关键操作 +} finally { + await myLock.release(); +} +``` + +## 在应用中的用法 + +在你的应用中如何使用 BrokerLock 的完整示例。 +如果导入 `FrameworkModule`,该类会自动在依赖注入容器中可用。 +更多信息参见“入门”页面。 + +```typescript +import { BrokerLock, BrokerLockItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 将此类型移动到一个共享文件中 +type MyCriticalLock = BrokerLockItem; + +class Service { + constructor(private criticalLock: MyCriticalLock) { + } + + async doSomethingCritical() { + await using hold = await this.criticalLock.hold(); + + // 执行关键操作, + // 锁会自动释放 + } +} + +const app = new App({ + providers: [ + Service, + provide<MyCriticalLock>((lock: BrokerLock) => lock.item('my-critical-lock', { ttl: '60s', timeout: '10s' })), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/broker/cache.md b/website/src/translations/zh/documentation/broker/cache.md new file mode 100644 index 000000000..d55069f1a --- /dev/null +++ b/website/src/translations/zh/documentation/broker/cache.md @@ -0,0 +1,89 @@ +# Broker 缓存 + +Deepkit Broker Cache 类是一个多级(2 级)缓存,它在内存中保留易失性的本地缓存,只有当数据不在缓存中、已过期或被失效时,才会从 broker 服务器获取数据。这样可以在数据获取时实现非常高的性能和低延迟。 + +该缓存被设计为类型安全,并会自动(使用 BSON)序列化和反序列化数据。它还支持缓存失效与清理。该实现确保在每个进程中,即使有多个请求同时尝试访问同一缓存项,缓存也只会重建一次。 + +数据不会持久化到服务器,仅保存在内存中。若服务器重启,所有数据都会丢失。 + +## 用法 + +请务必阅读入门页面,以了解如何正确设置应用,使 BrokerCache 在依赖注入容器中可用。 + +Deepkit Broker 中的缓存抽象与简单的键/值存储截然不同。它通过定义一个缓存名称和一个构建器函数工作:当缓存为空或过期时,会自动调用该函数。该构建器函数负责构建随后存储到缓存中的数据。 + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; + +const cache = new BrokerCache(adapter); + +const cacheItem = cache.item('my-cache', async () => { + // 当缓存为空或过期时会调用的构建器函数 + return 'hello world'; +}); + + +// 检查缓存是否过期或为空 +await cacheItem.exists(); + +// 从缓存中获取数据,或从 broker 服务器获取。 +// 如果缓存为空或过期,将调用构建器函数, +// 并返回结果,同时将结果发送到 broker 服务器。 +const topUsers = await cacheItem.get(); + +// 使缓存失效,以便下一次调用 get() 时 +// 再次调用构建器函数。 +// 会清理本地缓存和服务器缓存。 +await cacheItem.invalidate(); + +// 手动向缓存中设置数据 +await cacheItem.set(xy); +``` + +## 在应用中的用法 + +一个在应用中使用 BrokerCache 的完整示例。 +如果导入了 `FrameworkModule`,该类会自动在依赖注入容器中可用。 +更多信息参见入门页面。 + +```typescript +import { BrokerCache, BrokerCacheItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 最好将这些类型定义在一个公共文件中,以便复用 +// 并将它们注入到你的服务中 +type MyCacheItem = BrokerCacheItem<User[]>; + +function createMyCache(cache: BrokerCache, database: Database) { + return cache.item<User[]>('top-users', async () => { + // 当缓存为空或过期时会调用的构建器函数 + return await database.query(User) + .limit(10).orderBy('score').find(); + }); +} + +class Service { + constructor(private cacheItem: MyCacheItem) { + } + + async getTopUsers() { + return await this.cacheItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + Database, + provide<MyCacheItem>(createMyCache), + ], + imports: [ + new FrameworkModule(), + ], +}); + +const cacheItem = app.get<MyCacheItem>(); + +// 从缓存中获取数据,或从 broker 服务器获取 +const topUsers = await cacheItem.get(); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/broker/key-value.md b/website/src/translations/zh/documentation/broker/key-value.md new file mode 100644 index 000000000..902ab50b5 --- /dev/null +++ b/website/src/translations/zh/documentation/broker/key-value.md @@ -0,0 +1,91 @@ +# Broker 键值存储 + +Deepkit Broker Key-Value 类是一个与 broker 服务器配合工作的简单键/值存储抽象。它提供了一种从 broker 服务器存储和检索数据的简单方式。 + +未实现本地缓存。所有 `get` 调用每次都会向 broker 服务器发起真实的网络请求。若要避免此情况,请使用 Broker Cache 抽象。 + +数据不会在服务器上持久化,只会保存在内存中。如果服务器重启,所有数据都会丢失。 + +## 用法 + +```typescript +import { BrokerKeyValue } from '@deepkit/broker'; + +const keyValue = new BrokerKeyValue(adapter, { + ttl: '60s', // 每个键的生存时间。0 表示无 TTL(默认)。 +}); + +const item = keyValue.item<number>('key1'); + +await item.set(123); +console.log(await item.get()); //123 + +await item.remove(); +``` + +数据将根据给定的类型使用 BSON 自动序列化和反序列化。 + +方法 `set` 和 `get` 也可以直接在 `BrokerKeyValue` 实例上调用, +但缺点是你每次都需要传递键和类型。 + +```typescript +await keyValue.set<number>('key1', 123); +console.log(await keyValue.get<number>('key1')); //123 +``` + +## 自增 + +`increment` 方法允许你以原子方式按给定数值增加某个键的值。 + +请注意,它会在服务器上创建自己的存储条目,并且与 `set` 或 `get` 不兼容。 + +```typescript + +const activeUsers = keyValue.item<number>('activeUsers'); + +// 以原子方式加 1 +await activeUsers.increment(1); + +await activeUsers.increment(-1); + +// 获取当前值的唯一方式是以 0 调用 increment +const current = await activeUsers.increment(0); + +// 移除该条目 +await activeUsers.remove(); +``` + +## 在应用中使用 + +一个在你的应用中使用 BrokerKeyValue 的完整示例。 +如果导入 `FrameworkModule`,该类会自动在依赖注入容器中可用。 +更多信息请参阅入门页面。 + +```typescript +import { BrokerKeyValue, BrokerKeyValueItem } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 将此类型移动到一个共享文件中 +type MyKeyValueItem = BrokerKeyValueItem<User[]>; + +class Service { + constructor(private keyValueItem: MyKeyValueItem) { + } + + async getTopUsers(): Promise<User[]> { + // 可能为 undefined。你需要处理这种情况。 + // 如果你想避免这种情况,请使用 Broker Cache。 + return await this.keyValueItem.get(); + } +} + +const app = new App({ + providers: [ + Service, + provide<MyKeyValueItem>((keyValue: BrokerKeyValue) => keyValue.item<User[]>('top-users')), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/broker/message-bus.md b/website/src/translations/zh/documentation/broker/message-bus.md new file mode 100644 index 000000000..6c575bc0b --- /dev/null +++ b/website/src/translations/zh/documentation/broker/message-bus.md @@ -0,0 +1,137 @@ +# Broker 总线 + +Deepkit 消息总线是一种消息总线系统(发布/订阅,分布式事件系统),它允许你在应用的不同部分之间发送消息或事件。 + +它可用于微服务、单体或任何其他类型的应用。非常适合事件驱动架构。 + +它不同于 Deepkit 事件系统,后者用于进程内事件。Broker 总线用于需要发送到其他进程或服务器的事件。当你希望在由 FrameworkModule 自动启动的多个 worker 之间通信时,Broker 总线也非常适合,例如 `new FrameworkModule({workers: 4})`。 + +该系统旨在保持类型安全,并会自动序列化与反序列化消息(使用 BSON)。如果你为消息类型添加了[验证](../runtime-types/validation.md),它还会在发送前和接收后验证消息。这确保消息始终具有正确的格式并包含预期的数据。 + +## 用法 + +```typescript +import { BrokerBus } from '@deepkit/broker'; + +const bus = new BrokerBus(adapter); + +// 将此类型移动到共享文件 +type UserEvent = { type: 'user-created', id: number } | { type: 'user-deleted', id: number }; + +const channel = bus.channel<Events>('user-events'); + +await channel.subscribe((event) => { + if (event.type === 'user-created') { + console.log('User created', event.id); + } else if (event.type === 'user-deleted') { + console.log('User deleted', event.id); + } +}); + +await channel.publish({ type: 'user-created', id: 1 }); +``` + +通过为通道定义名称和类型,你可以确保只发送和接收正确类型的消息。 +数据会被自动序列化与反序列化(使用 BSON)。 + +## 在应用中使用 + +一个在你的应用中使用 BrokerBus 的完整示例。 +如果你导入 `FrameworkModule`,该类会自动在依赖注入容器中可用。 +有关更多信息,请参阅入门页面。 + +### Subject + +发送和监听消息的默认方式是使用 rxjs 的 `Subject` 类型。它的 `subscribe` 和 `next` 方法使你能够以类型安全的方式发送和接收消息。所有 Subject 实例都由代理管理,一旦该 Subject 被垃圾回收,其订阅将从代理后端(例如 Redis)中移除。 + +覆盖 BrokerBus 的 `BusBrokerErrorHandler` 以处理发布或订阅消息时的失败。 + +这种方法很好地将你的业务代码与代理服务器解耦,并允许你在没有代理服务器的测试环境中使用相同的代码。 + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusSubject } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; +import { Subject } from 'rxjs'; + +// 将此类型移动到共享文件 +type MyChannel = Subject<{ + id: number; + name: string; +}>; + +class Service { + // MyChannel 不是单例,而是为每个请求创建一个新实例。 + // 其生命周期由框架监控,一旦该 subject 被垃圾回收, + // 订阅将从代理后端(例如 Redis)中移除。 + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }); + } + + update() { + this.channel.next({ id: 1, name: 'Peter' }); + } +} + +@rpc.controller('my-controller') +class MyRpcController { + constructor(private channel: MyChannel) { + } + + @rpc.action() + getChannelData(): MyChannel { + return this.channel; + } +} + +const app = new App({ + controllers: [MyRpcController], + providers: [ + Service, + provideBusSubject<MyChannel>('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` + +### 总线通道 + +如果你需要确认消息已发送,并希望在每种情况下处理错误,可以使用 `BrokerBusChannel` 类型。它的 `subscribe` 和 `publish` 方法会返回一个 Promise。 + +```typescript +import { BrokerBus, BrokerBusChannel, provideBusChannel } from '@deepkit/broker'; +import { FrameworkModule } from '@deepkit/framework'; + +// 将此类型移动到共享文件 +type MyChannel = BrokerBusChannel<{ + id: number; + name: string; +}>; + +class Service { + constructor(private channel: MyChannel) { + this.channel.subscribe((message) => { + console.log('received message', message); + }).catch(e => { + console.error('Error while subscribing', e); + }); + } + + async update() { + await this.channel.publish({ id: 1, name: 'Peter' }); + } +} + +const app = new App({ + providers: [ + Service, + provideBusChannel<MyChannel>('my-channel'), + ], + imports: [ + new FrameworkModule(), + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/broker/message-queue.md b/website/src/translations/zh/documentation/broker/message-queue.md new file mode 100644 index 000000000..4ef1cbf34 --- /dev/null +++ b/website/src/translations/zh/documentation/broker/message-queue.md @@ -0,0 +1,118 @@ +# Broker 队列 + +Deepkit Message Queue 是一个消息队列系统,它允许你将消息发送到队列服务器并由 worker 进行处理。 + +该系统设计为类型安全,并自动序列化和反序列化消息(使用 BSON)。 + +数据持久化在服务器上,因此即使服务器崩溃,数据也不会丢失。 + +## 使用 + +```typescript +import { BrokerQueue, BrokerQueueChannel } from '@deepkit/broker'; + +const queue = new BrokerQueue(adapter); + +type User = { id: number, username: string }; + +const registrationChannel = queue.channel<User>('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +}); +``` + +```typescript +// 一个 worker 消费消息。 +// 这通常在单独的进程中完成。 +await registrationChannel.consume(async (user) => { + console.log('User registered', user); + // 如果 worker 在这里崩溃,消息不会丢失。 + // 它会自动重新投递给另一个 worker。 + // 如果这个回调未抛出错误就返回,该消息会被 + // 标记为已处理,并最终被移除。 +}); + +// 应用程序发送消息 +await registrationChannel.produce({ id: 1, username: 'Peter' }); +``` + +## 在应用中的使用 + +一个在应用中如何使用 BrokerQueue 的完整示例。 +如果你导入了 `FrameworkModule`,该类会自动在依赖注入容器中可用。 +更多信息请参见“入门”页面。 + +为了最大化利用队列系统,建议启动多个 worker 来消费消息。 +你会编写一个与主应用(可能有 http 路由等)不同的独立 `App`。 + +通过共享的应用模块共享通用服务。通道定义放在一个公共文件中,供应用的其余部分共享。 + +```typescript +// 文件:channels.ts + +export type RegistrationChannel = BrokerQueueChannel<User>; +export const registrationChannelProvider = provide<RegistrationChannel>((queue: BrokerQueue) => queue.channel<User>('user/registered', { + process: QueueMessageProcessing.exactlyOnce, + deduplicationInterval: '1s', +})); +``` + +```typescript +// 文件:worker.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +async function consumerCommand( + channel: RegistrationChannel, + database: Database) { + + await channel.consume(async (user) => { + // 对该用户执行一些操作, + // 例如存储信息、发送邮件等。 + }); + + // 与 broker 的连接会保持进程存活。 +} + +const app = new App({ + providers: [ + Database, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +app.command('consumer', consumerCommand); + +// 直接启动上面的 worker 命令 +void app.run('consumer'); +``` + +在你的应用中,你可以像这样生产消息: + +```typescript +// 文件:app.ts +import { RegistrationChannel, registrationChannelProvider } from './channels'; + +class Service { + constructor(private channel: RegistrationChannel) { + } + + async registerUser(user: User) { + await this.channel.produce(user); + } +} + +const app = new App({ + providers: [ + Service, + registrationChannelProvider, + ], + imports: [ + new FrameworkModule({}), + ], +}); + +void app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/dependency-injection.md b/website/src/translations/zh/documentation/dependency-injection.md new file mode 100644 index 000000000..9dabeed30 --- /dev/null +++ b/website/src/translations/zh/documentation/dependency-injection.md @@ -0,0 +1,197 @@ +# 依赖注入 + +依赖注入(DI)是一种设计模式,其中类和函数接收它们的依赖。它遵循控制反转(IoC)原则,有助于更好地分离复杂代码,从而显著提升可测试性、模块化以及清晰度。尽管还有其他用于应用 IoC 原则的设计模式,例如服务定位器模式,但 DI 已经确立为主导模式,尤其是在企业软件中。 + +为说明 IoC 原则,下面是一个示例: + +```typescript +import { HttpClient } from 'http-library'; + +class UserRepository { + async getUsers(): Promise<Users> { + const client = new HttpClient(); + return await client.get('/users'); + } +} +``` + +UserRepository 类将 HttpClient 作为其依赖。本身这个依赖并不值得大书特书,但问题在于 `UserRepository` 自己创建了 HttpClient。 +看起来将 HttpClient 的创建封装进 UserRepository 是个好主意,但事实并非如此。如果我们想替换 HttpClient 怎么办?如果我们想在单元测试中测试 UserRepository,而又不允许真实的 HTTP 请求发出,该怎么办?我们如何得知该类甚至使用了一个 HttpClient? + +## 控制反转 + +按照控制反转(IoC)的思路,下面是一个替代方案:在构造函数中将 HttpClient 作为显式依赖(也称构造函数注入)。 + +```typescript +class UserRepository { + constructor( + private http: HttpClient + ) {} + + async getUsers(): Promise<Users> { + return await this.http.get('/users'); + } +} +``` + +现在,创建 HttpClient 的责任不再在 UserRepository,而在于 UserRepository 的使用者。这就是控制反转(IoC)。控制被反转或颠倒。更具体地说,这段代码应用了依赖注入,因为依赖是被接收(注入)的,而不再由自身创建或请求。依赖注入只是 IoC 的一种变体。 + +## 服务定位器 + +除了 DI,服务定位器(SL)也是应用 IoC 原则的一种方式。它通常被认为是依赖注入的对立面,因为它是请求依赖,而不是接收依赖。如果在上述代码中像下面这样请求 HttpClient,这就被称为服务定位器模式。 + +```typescript +class UserRepository { + async getUsers(): Promise<Users> { + const client = locator.getHttpClient(); + return await client.get('/users'); + } +} +``` + +函数 `locator.getHttpClient` 可以取任意名称。替代方案可能是像 `useContext(HttpClient)`、`getHttpClient()`、`await import("client")` 这样的函数调用,或像 `container.get(HttpClient)`、`container.http` 这样的容器查询。导入一个全局则是服务定位器的一个稍有不同的变体,使用模块系统本身作为定位器: + +```typescript +import { httpClient } from 'clients' + +class UserRepository { + async getUsers(): Promise<Users> { + return await httpClient.get('/users'); + } +} +``` + +这些变体的共同点在于它们显式地请求 HttpClient 依赖,并且代码知道存在一个服务容器。它将你的代码与框架紧密耦合,这是你希望避免的,以保持代码整洁。 + +服务请求不仅可能出现在属性的默认值上,还可能在代码中部的某个地方发生。由于发生在代码中部意味着它不是类型接口的一部分,HttpClient 的使用被隐藏起来。根据请求 HttpClient 的变体不同,有时用另一个实现替换它会非常困难,甚至完全不可能。尤其在单元测试领域以及出于清晰性的考虑,这里可能会出现困难,因此在某些情况下,服务定位器现在被归类为反模式。 + +## 依赖注入 + +使用依赖注入,不再去请求依赖,而是由使用者显式提供,或由代码接收依赖。使用者无法访问任何服务容器,也不知道 `HttpClient` 是如何被创建或获取的。本质上,它使你的代码能够与 IoC 框架解耦,从而更干净。 + +它只声明自己需要一个 `HttpClient` 类型。依赖注入相对服务定位器的一个关键差异和优势在于:使用依赖注入的代码即便没有任何服务容器和服务标识系统也能很好地工作(你不需要给你的服务起一个名字)。它只是一个类型声明,即使脱离 IoC 框架上下文也同样适用。 + +正如先前的示例所示,依赖注入模式已经在那里应用了。具体地说,可以看到构造函数注入,因为依赖是在构造函数中声明的。因此,UserRepository 现在必须如下实例化。 + +```typescript +const users = new UserRepository(new HttpClient()); +``` + +想要使用 UserRepository 的代码也必须提供(注入)它的所有依赖。HttpClient 是每次都创建还是每次都使用同一个,现在由类的使用者决定,而不再由类本身决定。它不再像服务定位器那样被请求,或者像最初的示例那样由自身完全创建。这种流程的反转带来了多种优势: + +* 代码更易于理解,因为所有依赖都是显式可见的。 +* 代码更易于测试,因为所有依赖都是唯一的,并且在需要时可以轻松修改。 +* 代码更加模块化,因为依赖可以轻松替换。 +* 它促进关注点分离原则,因为 UserRepository 不再需要自己负责创建非常复杂的依赖。 + +但一个明显的缺点也可以直接看出来:我真的需要自己创建或管理像 HttpClient 这样的所有依赖吗?是,也不是。是,因为在许多情况下,自己管理依赖完全是合理的。一个良好 API 的标志是依赖不会失控,即便如此仍然令人愉快地使用。对于许多应用或复杂库,这很可能就是事实。为了以简化的方式向用户提供一个拥有众多依赖的非常复杂的底层 API,外观模式非常合适。 + +## 依赖注入容器 + +然而,对于更复杂的应用,没有必要自己管理所有依赖,因为这正是所谓依赖注入容器的用途。它不仅会自动创建所有对象,还会自动“注入”依赖,因此不再需要手动的 “new” 调用。注入有多种类型,例如构造函数注入、方法注入或属性注入。这样,即便是拥有许多依赖的复杂架构也可以轻松管理。 + +Deepkit 提供了依赖注入容器(也称 DI 容器或 IoC 容器),位于 `@deepkit/injector`,或通过 Deepkit Framework 中的 App 模块已集成就绪。使用 `@deepkit/injector` 包的低层 API,上面的代码将如下所示。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders( + [UserRepository, HttpClient] +); + +const userRepo = injector.get(UserRepository); + +const users = await userRepo.getUsers(); +``` + +在这个例子中,`injector` 对象就是依赖注入容器。容器使用 `get(UserRepository)` 返回一个 UserRepository 实例,而不是使用 “new UserRepository”。为了静态初始化容器,会将提供者列表传递给 `InjectorContext.forProviders` 函数(在本例中就是这些类)。 +由于 DI 的核心是提供依赖,因此要将依赖提供给容器,这也就是“provider(提供者)”这一术语的由来。 + +提供者有多种类型:ClassProvider、ValueProvider、ExistingProvider、FactoryProvider。它们结合起来,使得可以用 DI 容器映射出非常灵活的架构。 + +提供者之间的所有依赖都会被自动解析,一旦发生 `injector.get()` 调用,对象和依赖就会被创建、缓存,并被正确地作为构造函数参数传递(称为构造函数注入)、设置为属性(称为属性注入),或传递给方法调用(称为方法注入)。 + +现在要将 HttpClient 替换为另一个实现,可以为 HttpClient 定义另一个提供者(此处是 ValueProvider): + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useValue: new AnotherHttpClient()}, +]); +``` + +一旦通过 `injector.get(UserRepository)` 请求 UserRepository,它就会接收到 AnotherHttpClient 对象。或者,也可以很好地使用 ClassProvider,这样 AnotherHttpClient 的所有依赖也由 DI 容器管理。 + +```typescript +const injector = InjectorContext.forProviders([ + UserRepository, + {provide: HttpClient, useClass: AnotherHttpClient}, +]); +``` + +所有类型的提供者都在[依赖注入提供者](./dependency-injection/providers.md)一节中列出并解释。 + +需要在此提及的是,Deepkit 的 DI 容器仅与 Deepkit 的运行时类型一起工作。这意味着,任何包含类、类型、接口和函数的代码都必须由 Deepkit Type Compiler 编译,以便在运行时拥有类型信息。参见章节[运行时类型](./runtime-types.md)。 + +## 依赖反转 + +前面的 UserRepository 示例显示,UserRepository 依赖于更低层的 HTTP 库。此外,声明为依赖的是具体实现(类),而不是抽象(接口)。乍一看,这似乎符合面向对象范式,但在复杂和大型架构中可能会导致问题。 + +另一种做法是将 HttpClient 依赖转换为抽象(接口),从而不再将 HTTP 库的代码导入到 UserRepository 中。 + +```typescript +interface HttpClientInterface { + get(path: string): Promise<any>; +} + +class UserRepository { + concstructor( + private http: HttpClientInterface + ) {} + + async getUsers(): Promise<Users> { + return await this.http.get('/users'); + } +} +``` + +这被称为依赖反转原则。UserRepository 不再直接依赖 HTTP 库,而是基于抽象(接口)。它因此解决了该原则中的两个基本目标: + +* 高层模块不应从低层模块导入任何东西。 +* 实现应当基于抽象(接口)。 + +现在可以通过 DI 容器将这两个实现(带有 HTTP 库的 UserRepository)合并在一起。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); +``` + +由于 Deepkit 的 DI 容器能够解析抽象依赖(接口),例如这里的 HttpClientInterface,UserRepository 会自动获得 HttpClient 的实现,因为 HttpClient 实现了接口 HttpClientInterface。 + +这可以通过 HttpClient 明确实现 HttpClientInterface(`class HttpClient implements HttpClientInterface`),或者 HttpClient 的 API 仅仅与 HttpClientInterface 兼容来实现。 + +一旦 HttpClient 修改了它的 API(例如移除了 `get` 方法),从而不再与 HttpClientInterface 兼容,DI 容器将抛出一个错误("the HttpClientInterface dependency was not provided")。此时,想要将这两个实现组合起来的使用者有义务找到解决方案。举例来说,可以在这里注册一个适配器类,该类实现 HttpClientInterface,并将方法调用正确地转发到 HttpClient。 + +作为替代方案,也可以直接为 HttpClientInterface 提供一个具体实现。 + +```typescript +import { InjectorContext, provide } from '@deepkit/injector'; +import { HttpClient } from './http-client'; +import { UserRepository, HttpClientInterface } from './user-repository'; + +const injector = InjectorContext.forProviders([ + UserRepository, + provide<HttpClientInterface>({useClass: HttpClient}), +]); +``` + +需要注意的是,尽管理论上依赖反转原则具有其优势,但在实践中也有显著的劣势。它不仅会导致更多代码(因为必须编写更多接口),还会带来更多复杂性(因为每个实现现在对每个依赖都有一个接口)。只有当应用达到一定规模并需要这种灵活性时,这种代价才值得付出。与任何设计模式和原则一样,这一原则也有其成本-收益权衡,在应用之前应当深思熟虑。 + +设计模式不应被盲目且一刀切地用于最简单的代码。然而,当具备复杂架构、大型应用或团队扩张等前提条件时,依赖反转和其他设计模式才能真正发挥其力量。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/dependency-injection/configuration.md b/website/src/translations/zh/documentation/dependency-injection/configuration.md new file mode 100644 index 000000000..061626c5f --- /dev/null +++ b/website/src/translations/zh/documentation/dependency-injection/configuration.md @@ -0,0 +1,92 @@ +# 配置 + +依赖注入容器也允许注入配置选项。此配置注入可以通过构造函数注入或属性注入接收。 + +模块 API 支持定义一个配置定义,它是一个常规类。通过为该类提供属性,每个属性都充当一个配置选项。得益于 TypeScript 对类的定义方式,这使得可以为每个属性定义类型和默认值。 + +```typescript +class RootConfiguration { + domain: string = 'localhost'; + debug: boolean = false; +} + +const rootModule = new InjectorModule([UserRepository]) + .setConfigDefinition(RootConfiguration) + .addImport(lowLevelModule); +``` + +现在可以在提供者中非常方便且类型安全地使用配置选项 `domain` 和 `debug`。 + +```typescript +class UserRepository { + constructor(private debug: RootConfiguration['debug']) {} + + getUsers() { + if (this.debug) console.debug('fetching users ...'); + } +} +``` + +这些选项的值可以通过 `configure()` 进行设置。 + +```typescript + rootModule.configure({debug: true}); +``` + +没有默认值但仍然必需的选项可以使用 `!` 标记。这会强制模块的使用者提供该值,否则将发生错误。 + +```typescript +class RootConfiguration { + domain!: string; +} +``` + +## 验证 + +此外,前面章节 [验证](../runtime-types/validation.md) 和 [序列化](../runtime-types/serialization.md) 中的所有序列化与验证类型都可以用来详细指定某个选项必须满足的类型与内容约束。 + +```typescript +class RootConfiguration { + domain!: string & MinLength<4>; +} +``` + +## 注入 + +与其他依赖一样,配置选项也可以如前所示通过 DI 容器安全且便捷地注入。最简单的方法是使用索引访问操作符引用单个选项: + +```typescript +class WebsiteController { + constructor(private debug: RootConfiguration['debug']) {} + + home() { + if (this.debug) console.debug('visit home page'); + } +} +``` + +配置选项既可以单独引用,也可以成组引用。为此可使用 TypeScript 的工具类型 `Partial`: + +```typescript +class WebsiteController { + constructor(private options: Partial<RootConfiguration, 'debug' | 'domain'>) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +要获取所有配置选项,也可以直接引用配置类: + +```typescript +class WebsiteController { + constructor(private options: RootConfiguration) {} + + home() { + if (this.options.debug) console.debug('visit home page'); + } +} +``` + +不过,推荐仅引用实际使用到的配置选项。这样不仅简化单元测试,也更便于从代码中看出实际所需内容。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/dependency-injection/getting-started.md b/website/src/translations/zh/documentation/dependency-injection/getting-started.md new file mode 100644 index 000000000..5c98312df --- /dev/null +++ b/website/src/translations/zh/documentation/dependency-injection/getting-started.md @@ -0,0 +1,110 @@ +# 入门 + +由于 Deepkit 中的依赖注入基于运行时类型(Runtime Types),因此需要先正确安装运行时类型。参见 [运行时类型](../runtime-types/getting-started.md)。 + +一旦完成,便可安装 `@deepkit/injector`,或者安装已在内部使用该库的 Deepkit 框架。 + +```sh + npm install @deepkit/injector +``` + +安装完成后,即可直接使用其 API。 + + +## 用法 + +现在使用依赖注入有三种方式。 + +* 注入器 API(底层) +* 模块 API +* 应用 API(Deepkit 框架) + +如果在不使用 Deepkit 框架的情况下使用 `@deepkit/injector`,推荐前两种方式。 + +### 注入器 API + +注入器 API 已在[依赖注入简介](../dependency-injection)中介绍。其特点是通过单个类 `InjectorContext` 简单使用,创建一个 DI 容器,特别适合无模块的简单应用。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +const injector = InjectorContext.forProviders([ + UserRepository, + HttpClient, +]); + +const repository = injector.get(UserRepository); +``` + +在此情况下,`injector` 对象即为依赖注入容器。函数 `InjectorContext.forProviders` 接受一个提供者数组。参见[依赖注入提供者](dependency-injection.md#di-providers)了解可传递的值。 + +### 模块 API + +更复杂的 API 是 `InjectorModule` 类,它允许将提供者存放在不同模块中,从而为每个模块创建多个封装的 DI 容器。此外,它还允许为每个模块使用配置类,使得向提供者提供经过自动验证的配置值更容易。模块之间可以相互导入,提供者可导出,从而构建层次化、良好分离的架构。 + +如果应用更复杂且未使用 Deepkit 框架,应使用此 API。 + +```typescript +import { InjectorModule, InjectorContext } from '@deepkit/injector'; + +const lowLevelModule = new InjectorModule([HttpClient]) + .addExport(HttpClient); + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +const injector = new InjectorContext(rootModule); +``` + +在此情况下,`injector` 对象即为依赖注入容器。提供者可以拆分到不同模块中,并通过模块导入在不同位置再次引入。这样就会形成一个自然的层次结构,反映应用或架构的层级。 +应始终将层级中的顶层模块(也称根模块或应用模块)传给 InjectorContext。此时 InjectorContext 仅充当中间角色:对 `injector.get()` 的调用会被简单地转发到根模块。不过,也可以通过将模块作为第二个参数传入,从非根模块获取提供者。 + +```typescript +const repository = injector.get(UserRepository); + +const httpClient = injector.get(HttpClient, lowLevelModule); +``` + +所有非根模块默认都是封装的,因此该模块中的所有提供者仅对自身可用。若希望某个提供者可供其他模块使用,必须将其导出。导出后,该提供者会提升到层级中的父模块,从而以这种方式被使用。 + +若要默认将所有提供者导出到顶层(根模块),可使用 `forRoot` 选项。这样所有提供者即可被其他模块使用。 + +```typescript +const lowLevelModule = new InjectorModule([HttpClient]) + .forRoot(); //将所有提供者导出到根模块 +``` + +### 应用 API + +一旦使用 Deepkit 框架,模块将通过 `@deepkit/app` API 定义。它基于模块 API,因此前述能力同样适用。此外,还可以使用强大的钩子并定义配置加载器,以实现更为动态的架构。 + +详见[框架模块](../app/modules.md)一章。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry, HttpBody } from '@deepkit/http'; + +interface User { + username: string; +} + +class Service { + users: User[] = []; +} + +const app = new App({ + providers: [Service], + imports: [new FrameworkModule()], +}); + +const router = app.get(HttpRouterRegistry); + +router.post('/users', (body: HttpBody<User>, service: Service) => { + service.users.push(body); +}); + +router.get('/users', (service: Service): Users => { + return service.users; +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/dependency-injection/injection.md b/website/src/translations/zh/documentation/dependency-injection/injection.md new file mode 100644 index 000000000..da8b917ae --- /dev/null +++ b/website/src/translations/zh/documentation/dependency-injection/injection.md @@ -0,0 +1,71 @@ +# 注入 + +它被称为依赖注入(Dependency Injection),因为依赖被注入。注入可以由用户(手动)或由 DI 容器(自动)完成。 + +## 构造函数注入 + +在大多数情况下,使用构造函数注入。所有依赖都作为构造函数参数指定,并由 DI 容器自动注入。 + +```typescript +class MyService { + constructor(protected database: Database) { + } +} +``` + +可选依赖应标记为可选,否则在找不到提供者时可能会触发错误。 + +```typescript +class MyService { + constructor(protected database?: Database) { + } +} +``` + +## 属性注入 + +构造函数注入的替代方案是属性注入。这通常用于依赖是可选的,或构造函数已经过于臃肿的情况。实例创建后(因此构造函数已执行),这些属性会被自动赋值。 + +```typescript +import { Inject } from '@deepkit/core'; + +class MyService { + // 必需 + protected database!: Inject<Database>; + + // 或可选 + protected database?: Inject<Database>; +} +``` + +## 参数注入 + +在多种场景中你可以定义回调函数,例如用于 HTTP 路由或 CLI 命令。在这种情况下,你可以将依赖定义为参数。 +它们会由 DI 容器自动注入。 + +```typescript +import { Database } from './db'; + +app.get('/', (database: Database) => { + //... +}); +``` + +## 注入器上下文 + +若需要动态解析依赖,可以注入 `InjectorContext` 并使用它来获取依赖。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class MyService { + constructor(protected context: InjectorContext) { + } + + getDatabase(): Database { + return this.context.get(Database); + } +} +``` + +这在处理[依赖注入作用域](./scopes.md)时尤其有用。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/dependency-injection/providers.md b/website/src/translations/zh/documentation/dependency-injection/providers.md new file mode 100644 index 000000000..f83ee4dff --- /dev/null +++ b/website/src/translations/zh/documentation/dependency-injection/providers.md @@ -0,0 +1,311 @@ +# 提供者 + +在依赖注入(DI)容器中有多种方式提供依赖。最简单的方式是直接指定一个类。这也称为简写的 ClassProvider。 + +```typescript +new App({ + providers: [UserRepository] +}); +``` + +这是一种特殊的提供者,因为只需要指定类。其他所有提供者都必须以对象字面量的形式指定。 + +默认情况下,所有提供者都被标记为单例(singleton),因此任意时刻只会存在一个实例。若要在每次部署提供者时都创建一个新实例,可以使用 `transient` 选项。这将导致每次都重新创建类或每次都执行工厂函数。 + +```typescript +new App({ + providers: [{ provide: UserRepository, transient: true }] +}); +``` + +## 类提供者(ClassProvider) + +除了简写的 ClassProvider 之外,还有常规的 ClassProvider,它用对象字面量而不是类来表示。 + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: UserRepository }] +}); +``` + +这等价于以下两种写法: + +```typescript +new App({ + providers: [{ provide: UserRepository }] +}); + +new App({ + providers: [UserRepository] +}); +``` + +它可用于将某个提供者替换为另一个类。 + +```typescript +new App({ + providers: [{ provide: UserRepository, useClass: OtherUserRepository }] +}); +``` + +在这个示例中,`OtherUserRepository` 类也会被 DI 容器管理,并且其所有依赖会被自动解析。 + +## 值提供者(ValueProvider) + +可以使用该提供者提供静态值。 + +```typescript +new App({ + providers: [{ provide: OtherUserRepository, useValue: new OtherUserRepository() }] +}); +``` + +由于不仅类实例可以作为依赖提供,任何值都可以作为 `useValue` 指定。符号或原始类型(string、number、boolean)也可以用作提供者令牌。 + +```typescript +new App({ + providers: [{ provide: 'domain', useValue: 'localhost' }] +}); +``` + +原始类型的提供者令牌必须使用 Inject 类型声明为依赖。 + +```typescript +import { Inject } from '@deepkit/core'; + +class EmailService { + constructor(public domain: Inject<string, 'domain'>) {} +} +``` + +注入别名与原始类型提供者令牌的组合也可用于为不包含运行时类型信息的包提供依赖。 + +```typescript +import { Inject } from '@deepkit/core'; +import { Stripe } from 'stripe'; + +export type StripeService = Inject<Stripe, '_stripe'>; + +new App({ + providers: [{ provide: '_stripe', useValue: new Stripe }] +}); +``` + +然后在使用方按如下方式声明: + +```typescript +class PaymentService { + constructor(public stripe: StripeService) {} +} +``` + +## 现有提供者(ExistingProvider) + +可以定义转发到已定义的提供者。 + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useValue: new OtherUserRepository()}, + {provide: UserRepository, useExisting: OtherUserRepository} + ] +}); +``` + +## 工厂提供者(FactoryProvider) + +可以使用函数为提供者提供一个值。该函数还可以包含参数,而这些参数将由 DI 容器提供。因此,可以访问其他依赖或配置选项。 + +```typescript +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: () => { + return new OtherUserRepository() + }}, + ] +}); + +new App({ + providers: [ + {provide: OtherUserRepository, useFactory: (domain: RootConfiguration['domain']) => { + return new OtherUserRepository(domain); + }}, + ] +}); + +new App({ + providers: [ + Database, + {provide: OtherUserRepository, useFactory: (database: Database) => { + return new OtherUserRepository(database); + }}, + ] +}); +``` + +## 接口提供者(InterfaceProvider) + +除了类和原始类型外,还可以提供抽象(接口)。这是通过 `provide` 函数完成的,尤其在要提供的值不包含任何类型信息时非常有用。 + +```typescript +import { provide } from '@deepkit/injector'; + +interface Connection { + write(data: Uint16Array): void; +} + +class Server { + constructor (public connection: Connection) {} +} + +class MyConnection { + write(data: Uint16Array): void {} +} + +new App({ + providers: [ + Server, + provide<Connection>(MyConnection) + ] +}); +``` + +如果有多个提供者实现了 Connection 接口,将使用最后一个提供者。 + +作为 provide() 的参数,其他所有类型的提供者都可以使用。 + +```typescript +const myConnection = {write: (data: any) => undefined}; + +new App({ + providers: [ + provide<Connection>({ useValue: myConnection }) + ] +}); + +new App({ + providers: [ + provide<Connection>({ useFactory: () => myConnection }) + ] +}); +``` + +## 异步提供者 + +`@deepkit/injector` 的设计不支持与异步依赖注入容器一起使用异步提供者。这是因为请求提供者也需要是异步的,从而要求整个应用在最高层级以异步方式运行。 + +要进行异步初始化,应将该初始化移动到应用服务器的引导流程,因为那里事件可以是异步的。或者,也可以手动触发初始化。 + +## 配置提供者 + +配置回调允许对提供者的结果进行操作。例如,这对于使用另一种依赖注入方式(方法注入)非常有用。 + +它们只能与模块 API 或应用 API 一起使用,并在模块上注册。 + +```typescript +class UserRepository { + private db?: Database; + setDatabase(db: Database) { + this.db = db; + } +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider<UserRepository>(v => { + v.setDatabase(db); +}); +``` + +`configureProvider` 在回调中将 `v` 作为第一个参数传入,即 UserRepository 实例,可以在其上调用方法。 + +除了调用方法,还可以设置属性。 + +```typescript +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository]) + .addImport(lowLevelModule); + +rootModule.configureProvider<UserRepository>(v => { + v.db = new Database(); +}); +``` + +所有回调都会被放入队列,并按定义顺序执行。 + +一旦提供者被创建,队列中的调用就会在提供者的实际结果上执行。也就是说,对于 ClassProvider,这些调用会在类实例创建后应用到该实例上;对于 FactoryProvider,则应用到工厂的返回结果上;对于 ValueProvider,则应用到该值上。 + +为了引用不仅是静态值,还包括其他提供者,可以通过在回调参数中声明它们,将任意依赖注入到回调中。请确保这些依赖在提供者的作用域中是已知的。 + +```typescript +class Database {} + +class UserRepository { + db?: Database; +} + +const rootModule = new InjectorModule([UserRepository, Database]) +rootModule.configureProvider<UserRepository>((v, db: Database) => { + v.db = db; +}); +``` + +## 名义类型 + +请注意,传递给 `configureProvider` 的类型(例如上一个示例中的 `UserRepository`)不是通过结构类型检查解析的,而是通过名义类型解析的。这意味着具有相同结构但身份不同的两个类/接口不兼容。对于 `get<T>` 调用或解析依赖时也是如此。 + +这与 TypeScript 基于结构类型检查的工作方式不同。做出这种设计决策是为了避免意外的错误配置(例如请求一个空类,它在结构上与任何类都兼容),并使代码更加健壮。 + +在以下示例中,`User1` 和 `User2` 类在结构上兼容,但在名义上不兼容。这意味着请求 `User1` 时不会解析为 `User2`,反之亦然。 + +```typescript + +class User1 { + name: string = ''; +} + +class User2 { + name: string = ''; +} + +new App({ + providers: [User1, User2] +}); +``` + +通过继承类和实现接口可以建立名义关系。 + +```typescript +class UserBase { + name: string = ''; +} + +class User extends UserBase { +} + +const app = new App({ + providers: [User2] +}); + +app.get(UserBase); // 返回 User +``` + +```typescript +interface UserInterface { + name: string; +} + +class User implements UserInterface { + name: string = ''; +} + +const app = new App({ + providers: [User] +}); + +app.get<UserInterface>(); // 返回 User +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/dependency-injection/scopes.md b/website/src/translations/zh/documentation/dependency-injection/scopes.md new file mode 100644 index 000000000..355806999 --- /dev/null +++ b/website/src/translations/zh/documentation/dependency-injection/scopes.md @@ -0,0 +1,54 @@ +# 作用域 + +默认情况下,DI 容器的所有提供者都是单例,因此只会被实例化一次。这意味着在 UserRepository 的示例中,整个运行期间始终只有一个 UserRepository 实例。除非用户使用“new”关键字手动创建,否则在任何时候都不会创建第二个实例。 + +然而,在各种用例中,提供者应该只在短时间内或仅在某个特定事件期间被实例化。这样的事件例如可以是 HTTP 请求或 RPC 调用。这意味着每个事件都会创建一个新实例,并且当该实例不再被使用时会自动移除(由垃圾回收器完成)。 + +HTTP 请求是作用域的经典示例。例如,会话、用户对象或其他与请求相关的提供者可以注册到该作用域。要创建一个作用域,只需选择任意作用域名称,然后在提供者中为其指定该作用域。 + +```typescript +import { InjectorContext } from '@deepkit/injector'; + +class UserSession {} + +const injector = InjectorContext.forProviders([ + {provide: UserSession, scope: 'http'} +]); +``` + +一旦指定了作用域,该提供者就不能直接从 DI 容器中获取,因此下面的调用会失败: + +```typescript +const session = injector.get(UserSession); // 抛出错误 +``` + +相反,必须创建一个带作用域的 DI 容器。这会在每次收到 HTTP 请求时发生: + +```typescript +const httpScope = injector.createChildScope('http'); +``` + +在该作用域中注册的提供者现在可以通过这个带作用域的 DI 容器获取,也包括所有未定义作用域的提供者。 + +```typescript +const session = httpScope.get(UserSession); // 可行 +``` + +由于所有提供者默认都是单例,对于每个带作用域的容器,每次调用 `get(UserSession)` 都会返回同一个实例。如果你创建多个带作用域的容器,将会创建多个 UserSession 实例。 + +带作用域的 DI 容器可以从外部动态设置值。例如,在 HTTP 作用域中,可以很容易地设置 HttpRequest 和 HttpResponse 对象。 + +```typescript +const injector = InjectorContext.forProviders([ + {provide: HttpResponse, scope: 'http'}, + {provide: HttpRequest, scope: 'http'}, +]); + +httpServer.on('request', (req, res) => { + const httpScope = injector.createChildScope('http'); + httpScope.set(HttpRequest, req); + httpScope.set(HttpResponse, res); +}); +``` + +使用 Deepkit 框架的应用默认拥有 `http`、`rpc` 和 `cli` 作用域。分别参见章节 [CLI](../cli.md)、[HTTP](../http.md) 或 [RPC](../rpc.md)。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem.md b/website/src/translations/zh/documentation/filesystem.md new file mode 100644 index 000000000..07e06ba04 --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem.md @@ -0,0 +1,273 @@ +# Deepkit 文件系统 + +Deepkit 文件系统是一个用于本地和远程文件系统的抽象层。它让你以统一的方式处理文件和目录,无论文件存储在本地还是在远程服务器上。 + +开箱即用的受支持文件系统: + +- 本地文件系统(包含于 `@deepkit/filesystem`) +- 内存(包含于 `@deepkit/filesystem`) +- FTP(包含于 `@deepkit/filesystem-ftp`) +- SFTP(包含于 `@deepkit/filesystem-sftp`) +- AWS S3(包含于 `@deepkit/filesystem-aws-s3`) +- Google 云文件系统(包含于 `@deepkit/filesystem-google`) + +## 安装 + +```bash +npm install @deepkit/filesystem +``` + +注意:如果你不使用 NPM,请确保正确安装对等依赖。 + +## 用法 + +```typescript +import { Filesystem, FilesystemLocalAdapter } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter('/path/to/my/files'); +const filesystem = new Filesystem(adapter); + +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} + +await filesystem.write('myFile.txt', 'Hello World'); +const file = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); + +const content = await filesystem.read('myFile.txt'); +``` + + +## 列出文件 + +要列出文件,请使用 `files()` 方法。它返回一个 `File` 对象数组。 + +```typescript +const files = await filesystem.files(); +for (const file of files) { + console.log(file.path); +} +``` + +如果数组为空,要么该文件夹不存在,要么其中没有文件。 + +要递归列出所有文件,请使用 `allFiles()` 方法。 + +```typescript +const files = await filesystem.allFiles(); +for (const file of files) { + console.log(file.path); +} +``` + +## 读取文件 + +要读取文件,请使用 `read()` 方法。它以 `Uint8Array` 形式返回文件内容。 + +```typescript +const content = await filesystem.read('myFile.txt'); +``` + +要以文本方式读取,请使用 `readAsText()` 方法。它返回一个字符串。 + +```typescript +const content = await filesystem.readAsText('myFile.txt'); +``` + +## 写入文件 + +要写入文件,请使用 `write()` 方法。它接受 `Uint8Array` 或字符串。 + +```typescript +await filesystem.write('myFile.txt', 'Hello World'); +``` + +要从本地文件写入,请使用 `writeFile()` 方法。 + +注意,第一个参数是目录而不是文件名。文件名是所提供文件的基础名(basename)。 +你可以通过传递 {name: 'myFile.txt'} 对象作为第二个参数来更改文件名。如果未提供文件扩展名, +则会自动检测扩展名。 + +```typescript +const path = await filesystem.writeFile('/', { path: '/path/to/local/file.txt' }); + +// 适用于 UploadedFile +router.post('/upload', async (body: HttpBody<{ file: UploadedFile }>, filesystem: Filesystem, session: Session) => { + const user = session.getUser(); + const path = await filesystem.writeFile('/user-images', body.file, { name: `user-${user.id}` }); + // 路径 = /user-images/user-123.jpg +}); +``` + + +## 删除文件 + +要删除文件,请使用 `delete()` 方法。 + +```typescript +await filesystem.delete('myFile.txt'); +``` + +这将删除根文件夹中的 `myFile.txt`。如果该路径是一个目录,则会抛出错误。 + +## 删除目录 + +要删除目录,请使用 `deleteDirectory()` 方法。这会删除该目录以及其中的所有文件和子目录。 + +```typescript +await filesystem.deleteDirectory('myFolder'); +``` + +## 创建目录 + +要创建目录,请使用 `createDirectory()` 方法。 + +```typescript +await filesystem.makeDirectory('myFolder'); +``` + +## 移动文件 + +要移动文件或目录,请使用 `move()` 方法。它接受 `File` 对象或路径。 + +```typescript +await filesystem.move('myFile.txt', 'myFolder/myFile.txt'); +``` + +如果目录 `myFolder` 不存在,将会自动创建。 + +## 复制文件 + +要复制文件或目录,请使用 `copy()` 方法。它接受 `File` 对象或路径。 + +```typescript +await filesystem.copy('myFile.txt', 'myFolder/myFile.txt'); +``` + +如果目录 `myFolder` 不存在,将会自动创建。 + +## 文件信息 + +要获取文件信息,请使用 `get()` 方法。它返回一个 `File` 对象。 + +```typescript +const file: FilesystemFile = await filesystem.get('myFile.txt'); +console.log(file.path, file.size, file.lastModified); +``` + +它返回路径、以字节为单位的文件大小以及最后修改日期。如果适配器不支持,修改日期可能为 undefined。 + +File 对象还提供了一些方便的方法: + +```typescript +interface FilesystemFile { + path: string; + type: FileType; // 文件或目录 + size: number; + lastModified?: Date; + /** + * 文件的可见性。 + * + * 请注意,某些适配器可能不支持读取文件的可见性。 + * 在这种情况下,可见性始终为 'private'。 + * + * 有些适配器可能支持按文件读取可见性,但在列出文件时不支持。 + * 在这种情况下,你需要额外调用 `filesystem.get(file)` 来加载可见性。 + */ + visibility: FileVisibility; // 公共或私有 + constructor(path: string, type?: FileType); + /** + * 如果此文件是符号链接则返回 true。 + */ + isFile(): boolean; + /** + * 如果此文件是目录则返回 true。 + */ + isDirectory(): boolean; + /** + * 返回文件名(basename)。 + */ + get name(): string; + /** + * 如果此文件位于给定目录中则返回 true。 + */ + inDirectory(directory: string): boolean; + /** + * 返回文件的目录(dirname)。 + */ + get directory(): string; + /** + * 返回文件的扩展名;如果不存在或是目录,则返回空字符串。 + */ + get extension(): string; +} +``` + +## 文件是否存在 + +要检查文件是否存在,请使用 `exists()` 方法。 + +```typescript + +if (await filesystem.exists('myFile.txt')) { + console.log('File exists'); +} +``` + +## 文件可见性 + +Deepkit 文件系统支持一个简单的文件可见性抽象,可用于将文件设为公开或私有。 + +这对于 S3 或 Google 云文件系统等非常有用。对于本地文件系统,它会根据可见性设置文件权限。 + +要设置文件的可见性,请使用 `setVisibility()` 方法,或在调用 `write()` 时将可见性作为第三个参数传入。 + +```typescript +await filesystem.setVisibility('myFile.txt', 'public'); +await filesystem.write('myFile.txt', 'Hello World', 'public'); +``` + +要获取文件的可见性,请使用 `getVisibility()` 方法或查看 `FilesystemFile.visibility`。 + +```typescript +const visibility = await filesystem.getVisibility('myFile.txt'); +const file = await filesystem.get('myFile.txt'); +console.log(file.visibility); +``` + +请注意,某些适配器可能不支持从 `sotrage.files()` 获取可见性。在这种情况下,可见性始终为 `unknown`。 + +## 文件公共 URL + +要获取文件的公共 URL,请使用 `publicUrl()` 方法。仅当文件为公开且适配器支持时此方法才有效。 + +```typescript +const url = filesystem.publicUrl('myFile.txt'); +``` + +如果适配器不支持公共 URL,Filesystem 抽象会通过使用 `option.baseUrl` 来生成一个 URL。 + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + baseUrl: 'https://my-domain.com/assets/' +}); + +const url = await filesystem.publicUrl('myFile.txt'); +console.log(url); //https://my-domain.com/assets/myFile.txt +``` + +## 文件系统选项 + +`Filesystem` 构造函数接受一个带有选项的第二个参数。 + +```typescript +const filesystem = new Filesystem(new FilesystemLocalAdapter('/path/to/my/files'), { + visibility: 'private', // 文件的默认可见性 + directoryVisibility: 'private', // 目录的默认可见性 + pathNormalizer: (path: string) => path, // 规范化路径。默认将 `[^a-zA-Z0-9\.\-\_]` 替换为 `-`。 + urlBuilder: (path: string) => path, // 为文件构建公共 URL。默认返回 baseUrl + path + baseUrl: '', // 公共 URL 的基础地址 +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/app.md b/website/src/translations/zh/documentation/filesystem/app.md new file mode 100644 index 000000000..f17750217 --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/app.md @@ -0,0 +1,84 @@ +将以下文本翻译成中文: # App + +虽然 Deepkit Storage 可以独立运行,但你可能希望在 Deepkit App 中通过依赖注入来使用它。 + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +const app = new App({ + providers: [ + provideFilesystem(new FilesystemLocalAdapter({root: __dirname + '/public'})), + ] +}); + +app.command('write', async (content: string, fs: Filesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: Filesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + + +## 多个文件系统 + +你可以同时使用多个文件系统。为此,你可以用 `provideNamedFilesystem('name', ...)` 注册它们,并通过 +`NamedFilesystem<'name'>` 获取 Filesystem 实例。 + +```typescript +import { App } from '@deepkit/app'; +import { NamedFilesystem, FilesystemLocalAdapter, provideNamedFilesystem } from '@deepkit/filesystem'; + +type PrivateFilesystem = NamedFilesystem<'private'>; +type PublicFilesystem = NamedFilesystem<'public'>; + +const app = new App({ + providers: [ + provideNamedFilesystem('private', new FilesystemLocalAdapter({root: '/tmp/dir1'})), + provideNamedFilesystem('public', new FilesystemLocalAdapter({root: '/tmp/dir2'})), + ] +}); + +app.command('write', async (content: string, fs: PublicFilesystem) => { + await fs.write('/hello.txt', content); +}); + +app.command('read', async (fs: PublicFilesystem) => { + const content = await fs.readAsText('/hello.txt'); + console.log(content); +}); + +app.run(); +``` + +## 配置 + +通常通过配置选项来配置适配器很有用。例如,你可能希望配置本地适配器的根目录,或 AWS S3 或 Google Storage 适配器的凭据。 + +为此,你可以使用[配置注入](../app/configuration.md)。 + +```typescript +import { App } from '@deepkit/app'; +import { Filesystem, FilesystemLocalAdapter, provideFilesystem } from '@deepkit/filesystem'; + +class MyConfig { + fsRoot: string = '/tmp'; +} + +const app = new App({ + config: MyConfig, + providers: [ + provideFilesystem( + (config: MyConfig) => new FilesystemLocalAdapter({root: config.fsRoot}) + ), + ] +}); + +app.loadConfigFromEnv({prefix: 'APP_'}) +app.run(); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/aws-s3.md b/website/src/translations/zh/documentation/filesystem/aws-s3.md new file mode 100644 index 000000000..8b0a8893a --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/aws-s3.md @@ -0,0 +1,50 @@ +# AWS S3 文件系统 + +AWS S3 文件系统适配器允许你将 AWS S3 服务用作 Deepkit 文件系统。 + +它属于需要单独安装的 `@deepkit/filesystem-aws-s3`。 + +```sh +npm install @deepkit/filesystem-aws-s3 +``` + +## 用法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemAwsS3Adapter } from '@deepkit/filesystem-aws-s3'; + +const adapter = new FilesystemAwsS3Adapter({ + bucket: 'my-bucket', + path: 'starting-path/', // 可选 + region: 'eu-central-1', + acccessKeyId: '...', + secretAccessKey: '...' +}); +const filesystem = new Filesystem(adapter); +``` + +注意:不应直接在代码中存储凭据。请使用环境变量或[应用配置](./app.md#configuration)。 + +该适配器使用 [@aws-sdk/client-s3](https://npmjs.com/package/@aws-sdk/client-s3) 的 S3 客户端。 +其所有配置选项都可以传递给适配器构造函数。 + +## 权限 + +你可以配置文件在创建时的可见性。 + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +文件 `/hello-public.txt` 将使用 ACL `public-read` 创建,并且任何人都可以通过其 URL 读取。可以使用 `filesystem.publicUrl` 获取该 URL。 + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://my-bucket.s3.eu-central-1.amazonaws.com/starting-path/hello-public.txt +``` + +要使用可见性功能,你必须在 S3 存储桶中启用基于对象的 ACL。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/database.md b/website/src/translations/zh/documentation/filesystem/database.md new file mode 100644 index 000000000..8dac0db63 --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/database.md @@ -0,0 +1,23 @@ +# 数据库文件系统 + +此适配器允许你使用数据库 ORM 作为文件系统后端。这意味着所有文件和文件夹都会存储在数据库中。 + +```sh +npm install @deepkit/filesystem-database @deepkit/orm +``` + +## 用法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemDatabaseAdapter } from '@deepkit/filesystem-database'; + +const database = new Database(new MemoryDatabaseAdapter()); +// const database = new Database(new PostgresDatabaseAdapter()); +// const database = new Database(new MongoDatabaseAdapter()); +// const database = new Database(new MysqlDatabaseAdapter()); +// const database = new Database(new SQLiteDatabaseAdapter()); + +const adapter = new FilesystemDatabaseAdapter({ database }); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/ftp.md b/website/src/translations/zh/documentation/filesystem/ftp.md new file mode 100644 index 000000000..795333ccc --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/ftp.md @@ -0,0 +1,55 @@ +# FTP 文件系统 + +此适配器允许你将 FTP 服务器用作文件系统。 + +它是 `@deepkit/filesystem-ftp` 的一部分,需要单独安装。 + +```sh +npm install @deepkit/filesystem-ftp +``` + +## 用法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemFtpAdapter } from '@deepkit/filesystem-ftp'; + +const adapter = new FilesystemFtpAdapter({ + root: 'folder', + host: 'localhost', + port: 21, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +注意:不应直接在代码中存储你的凭据。请使用环境变量或[应用配置](./app.md#configuration)。 + +## 权限 + +如果 FTP 服务器运行在类 Unix 环境中,你可以像使用[本地文件系统适配器](./local.md)那样,通过 `permissions` 选项设置文件和文件夹的权限。 + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +这里,文件 `/hello-public.txt` 将以权限 `0o644` 创建,而 `/hello-private.txt` 将以权限 `0o600` 创建。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/google-storage.md b/website/src/translations/zh/documentation/filesystem/google-storage.md new file mode 100644 index 000000000..04a336e70 --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/google-storage.md @@ -0,0 +1,49 @@ +# Google 存储文件系统 + +此适配器允许你将 Google 存储用作文件系统。 + +它是 `@deepkit/filesystem-google` 的一部分,需要单独安装。 + +```sh +npm install @deepkit/filesystem-google +``` + +## 用法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemGoogleAdapter } from '@deepkit/filesystem-google'; + +const adapter = new FilesystemGoogleAdapter({ + bucket: 'my-bucket', + path: 'starting-path/', // 可选 + projectId: '...', + keyFilename: 'path/to/service-account-key.json' +}); +const filesystem = new Filesystem(adapter); +``` + +注意:你不应直接在代码中存储凭据。相反,请使用环境变量或[应用配置](./app.md#configuration)。 + +该适配器使用 [@google-cloud/storage](https://npmjs.com/package/@google-cloud/storage) 的 Google 存储客户端。 +其所有配置选项都可以传递给适配器构造函数。 + +## 权限 + +你可以配置文件在创建时的可见性。 + +```typescript +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +文件 `/hello-public.txt` 将以 ACL `public: true` 创建,任何人都可以通过其 URL 读取。可以使用 `filesystem.publicUrl` 获取该 URL。 + +```typescript +const url = filesystem.publicUrl('/hello-public.txt'); +// https://storage.googleapis.com/my-bucket/starting-path/hello-public.txt +``` + +要使用可见性功能,你必须在 Google 存储的存储桶中启用基于对象的 ACL。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/local.md b/website/src/translations/zh/documentation/filesystem/local.md new file mode 100644 index 000000000..35ad6272f --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/local.md @@ -0,0 +1,49 @@ +# 本地文件系统 + +本地文件系统适配器是最常见的文件系统之一,提供对运行该应用程序的系统中文件系统的访问。 + +它是 `@deepkit/filesystem` 的一部分,并在底层使用 Node 的 `fs/promises` API,因此不需要额外安装。 + +## 用法 + +```typescript +import { FilesystemLocalAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemLocalAdapter({ root: '/path/to/files' }); +const filesystem = new Filesystem(adapter); +``` + +这里的 `root` 选项是文件系统的根目录。所有路径都相对于该根目录。 + +```typescript +// 读取文件 /path/to/files/hello.txt +const content: string = await filesystem.readAsText('/hello.txt'); +``` + +## 权限 + +你可以配置在创建文件和目录时文件系统应使用的权限。每个类别(文件、目录)可以分别配置为两种可见性:`public` 和 `private`。 + +```typescript +const adapter = new FilesystemLocalAdapter({ + root: '/path/to/files', + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +这里文件 `/hello-public.txt` 将以权限 `0o644` 创建,而 `/hello-private.txt` 为 `0o600`。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/memory.md b/website/src/translations/zh/documentation/filesystem/memory.md new file mode 100644 index 000000000..91b9c0ce9 --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/memory.md @@ -0,0 +1,15 @@ +# 内存文件系统 + +对于内存文件系统,文件系统存储在内存中。这意味着该文件系统不是持久的,并且在应用程序退出时会丢失。 +这对于测试用途尤其有用。 + +它是 `@deepkit/filesystem` 的一部分,因此无需额外安装。 + +## 用法 + +```typescript +import { FilesystemMemoryAdapter, Filesystem } from '@deepkit/filesystem'; + +const adapter = new FilesystemMemoryAdapter(); +const filesystem = new Filesystem(adapter); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/filesystem/sftp.md b/website/src/translations/zh/documentation/filesystem/sftp.md new file mode 100644 index 000000000..e036c4d65 --- /dev/null +++ b/website/src/translations/zh/documentation/filesystem/sftp.md @@ -0,0 +1,57 @@ +# sFTP(SSH)文件系统 + +此适配器允许你将 sFTP(SSH)服务器用作文件系统。 + +它是 `@deepkit/filesystem-sftp` 的一部分,需要单独安装。 + +```sh +npm install @deepkit/filesystem-sftp +``` + +## 用法 + +```typescript +import { Filesystem } from '@deepkit/filesystem'; +import { FilesystemSftpAdapter } from '@deepkit/filesystem-sftp'; + +const adapter = new FilesystemSftpAdapter({ + root: 'folder', + host: 'localhost', + port: 22, + username: 'user', + password: 'password', +}); +const filesystem = new Filesystem(adapter); +``` + +注意:你不应将凭据直接存储在代码中。相反,应使用环境变量或[应用配置](./app.md#configuration)。 + +此适配器使用 [ssh2-sftp-client](https://npmjs.com/package/ssh2-sftp-client) 的 sFTP 客户端。其所有配置选项都可以传递给该适配器的构造函数。 + +## 权限 + +如果 FTP 服务器运行在 Unix 环境中,你可以像[本地文件系统适配器](./local.md)那样,使用 `permissions` 选项设置文件和文件夹的权限。 + +```typescript +const adapter = new FilesystemFtpAdapter({ + // ... + permissions: { + file: { + public: 0o644, + private: 0o600, + }, + directory: { + public: 0o755, + private: 0o700, + } + } +}); + + +const filesystem = new Filesystem(adapter); + +filesystem.write('/hello-public.txt', 'hello world', 'public'); +filesystem.write('/hello-private.txt', 'hello world', 'private'); +``` + +这里,文件 `/hello-public.txt` 将以权限 `0o644` 创建,而 `/hello-private.txt` 将以权限 `0o600` 创建。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/framework.md b/website/src/translations/zh/documentation/framework.md new file mode 100644 index 000000000..1adb1ec83 --- /dev/null +++ b/website/src/translations/zh/documentation/framework.md @@ -0,0 +1,214 @@ +# Deepkit 框架 + +Deepkit 框架基于 `@deepkit/app` 中的 [Deepkit App](./app.md),并在 `@deepkit/framework` 中提供 `FrameworkModule` 模块,可在你的应用中导入。 + +`App` 抽象提供: + +- CLI 命令 +- 配置加载(环境变量、点文件(dotfiles)、自定义) +- 模块系统 +- 强大的服务容器 +- 用于控制器、提供者、监听器等的注册表和钩子 + +`FrameworkModule` 模块带来额外特性: + +- 应用服务器 + - HTTP 服务器 + - RPC 服务器 + - 多进程负载均衡 + - SSL +- 调试用 CLI 命令 +- 数据库迁移配置/命令 +- 通过 `{debug: true}` 选项提供调试/分析器 GUI +- 交互式 API 文档(类似 Swagger) +- DatabaseRegistry、ProcessLocking、Broker、Sessions 的提供者 +- 集成测试 API + +你可以使用或不使用 `FrameworkModule` 来编写应用。 + +## 安装 + +Deepkit 框架基于 [Deepkit App](./app.md)。请确保你已遵循其安装说明。 +之后你可以安装 Deepkit 框架并在你的 `App` 中导入 `FrameworkModule`。 + +```sh +npm install @deepkit/framework +``` + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + +app.run(); +``` + +由于应用现在导入了 `FrameworkModule`,你会看到有更多按主题分组的可用命令。 + +其中之一是 `server:start`,它会启动 HTTP 服务器。要使用它,我们需要至少注册一个 HTTP 路由。 + +```typescript +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule({ debug: true })] +}); + +app.command('test', (logger: Logger) => { + logger.log('Hello World!'); +}); + + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return 'Hello World'; +}) + +app.run(); +``` + +当你再次执行 `server:start` 命令时,你会看到 HTTP 服务器已经启动,并且路由 `/` 可用。 + +```sh +$ ./node_modules/.bin/ts-node ./app.ts server:start +``` + +```sh +$ curl http://localhost:8080/ +Hello World +``` + +要处理请求,请阅读 [HTTP](http.md) 或 [RPC](rpc.md) 章节。在 [App](app.md) 章节中你可以进一步了解 CLI 命令。 + +## App + +`App` 类是你的应用的主要入口。它负责加载所有模块、配置,并启动应用。 +它还负责加载并执行所有 CLI 命令。像 FrameworkModule 这样的模块会提供额外的命令、注册事件监听器、为 HTTP/RPC 提供控制器、服务提供者等。 + +该 `app` 对象也可在不运行 CLI 控制器的情况下用于访问依赖注入容器。 + +```typescript +const app = new App({ + imports: [new FrameworkModule] +}); + +// 访问所有已注册的服务 +const eventDispatcher = app.get(EventDispatcher); +``` + +你可以获取到 `EventDispatcher`,因为 `FrameworkModule` 将其与许多其他服务(Logger、ApplicationServer,以及[更多](https://github.com/deepkit/deepkit-framework/blob/master/packages/framework/src/module.ts))一起注册为服务提供者。 + +你也可以注册你自己的服务。 + +```typescript + +class MyService { + constructor(private logger: Logger) { + } + + helloWorld() { + this.logger.log('Hello World'); + } +} + +const app = new App({ + providers: [MyService], + imports: [new FrameworkModule] +}); + +const service = app.get(MyService); + +service.helloWorld(); +``` + +### 调试器 + +你的应用及所有模块的配置值可以在调试器中显示。启用 `FrameworkModule` 的 debug 选项,并打开 `http://localhost:8080/_debug/configuration`。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + debug: true, + }) + ] +}).run(); +``` + +![调试器配置](/assets/documentation/framework/debugger-configuration.png) + +你也可以使用 `ts-node app.ts app:config` 来显示所有可用的配置选项、当前值、默认值、描述和数据类型。 + +```sh +$ ts-node app.ts app:config +Application config +┌─────────┬───────────────┬────────────────────────┬────────────────────────┬─────────────┬───────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼───────────────┼────────────────────────┼────────────────────────┼─────────────┼───────────┤ +│ 0 │ 'pageTitle' │ 'Other title' │ 'Cool site' │ '' │ 'string' │ +│ 1 │ 'domain' │ 'example.com' │ 'example.com' │ '' │ 'string' │ +│ 2 │ 'port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 3 │ 'databaseUrl' │ 'mongodb://localhost/' │ 'mongodb://localhost/' │ '' │ 'string' │ +│ 4 │ 'email' │ false │ false │ '' │ 'boolean' │ +│ 5 │ 'emailSender' │ undefined │ undefined │ '' │ 'string?' │ +└─────────┴───────────────┴────────────────────────┴────────────────────────┴─────────────┴───────────┘ +Modules config +┌─────────┬──────────────────────────────┬─────────────────┬─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐ +│ (index) │ name │ value │ defaultValue │ description │ type │ +├─────────┼──────────────────────────────┼─────────────────┼─────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────┤ +│ 0 │ 'framework.host' │ 'localhost' │ 'localhost' │ '' │ 'string' │ +│ 1 │ 'framework.port' │ 8080 │ 8080 │ '' │ 'number' │ +│ 2 │ 'framework.httpsPort' │ undefined │ undefined │ 'If httpsPort and ssl is defined, then the https server is started additional to the http-server.' │ 'number?' │ +│ 3 │ 'framework.selfSigned' │ undefined │ undefined │ 'If for ssl: true the certificate and key should be automatically generated.' │ 'boolean?' │ +│ 4 │ 'framework.keepAliveTimeout' │ undefined │ undefined │ '' │ 'number?' │ +│ 5 │ 'framework.path' │ '/' │ '/' │ '' │ 'string' │ +│ 6 │ 'framework.workers' │ 1 │ 1 │ '' │ 'number' │ +│ 7 │ 'framework.ssl' │ false │ false │ 'Enables HTTPS server' │ 'boolean' │ +│ 8 │ 'framework.sslOptions' │ undefined │ undefined │ 'Same interface as tls.SecureContextOptions & tls.TlsOptions.' │ 'any' │ +... +``` + +## 应用服务器 + +## 文件结构 + +## 自动 CRUD + +## 事件 + +Deepkit 框架带有多种事件令牌,可以在其上注册事件监听器。 + +参见 [事件](./app/events.md) 章节以了解事件的工作方式。 + +### 派发事件 + +事件通过 `EventDispatcher` 类发送。在 Deepkit 应用中,这可以通过依赖注入提供。 + +```typescript +import { cli, Command } from '@deepkit/app'; +import { EventDispatcher } from '@deepkit/event'; + +@cli.controller('test') +export class TestCommand implements Command { + constructor(protected eventDispatcher: EventDispatcher) { + } + + async execute() { + this.eventDispatcher.dispatch(UserAdded, new UserEvent({ username: 'Peter' })); + } +} +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/framework/database.md b/website/src/translations/zh/documentation/framework/database.md new file mode 100644 index 000000000..8023df7bb --- /dev/null +++ b/website/src/translations/zh/documentation/framework/database.md @@ -0,0 +1,225 @@ +# 数据库 + +Deepkit 具有自己强大的数据库抽象库,称为 Deepkit ORM。它是一款对象关系映射(ORM)库,可简化与 SQL 数据库和 MongoDB 的协作。 + +虽然你可以使用任意数据库库,但我们推荐 Deepkit ORM,因为它是最快的 TypeScript 数据库抽象库,与 Deepkit 框架深度集成,并拥有许多能提升工作流程与效率的特性。 + +本章解释如何在你的 Deepkit 应用中使用 Deepkit ORM。有关 Deepkit ORM 的完整信息,请参阅[ORM](../orm.md)章节。 + +## 数据库类 + +在应用中使用 Deepkit ORM 的 `Database` 对象的最简单方式,是注册一个从其派生的类。 + +```typescript +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + constructor() { + super( + new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), + [User] + ); + } +} +``` + +创建一个新类,并在其构造函数中指定适配器及其参数;同时在第二个参数中添加所有应连接到此数据库的实体模型。 + +现在你可以将该数据库类注册为提供者(provider)。我们还启用 `migrateOnStartup`,它会在启动时自动在你的数据库中创建所有表。这非常适合快速原型开发,但不建议用于严肃项目或生产环境。此时应使用常规的数据库迁移。 + +我们还启用 `debug`,这样在应用服务器启动时可以打开调试器,并在内置的 ORM 浏览器中直接管理你的数据库模型。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { SQLiteDatabase } from './database.ts'; + +new App({ + providers: [SQLiteDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}).run(); +``` + +现在你可以通过依赖注入在任意位置访问 `SQLiteDatabase`: + +```typescript +import { SQLiteDatabase } from './database.ts'; + +export class Controller { + constructor(protected database: SQLiteDatabase) {} + + @http.GET() + async startPage(): Promise<User[]> { + //返回所有用户 + return await this.database.query(User).find(); + } +} +``` + +## 配置 + +在许多情况下,你希望连接凭据是可配置的。比如,测试环境与生产环境使用不同的数据库。你可以通过 `Database` 类的 `config` 选项来实现。 + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { PostgresDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +type DbConfig = Pick<AppConfig, 'databaseHost', 'databaseUser', 'databasePassword'>; + +export class MainDatabase extends Database { + constructor(config: DbConfig) { + super(new PostgresDatabaseAdapter({ + host: config.databaseHost, + user: config.databaseUser, + password: config.databasePassword, + }), [User]); + } +} +``` + +```typescript +//config.ts +export class AppConfig { + databaseHost: string = 'localhost'; + databaseUser: string = 'postgres'; + databasePassword: string = ''; +} +``` + +```typescript +//app.ts +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { MainDatabase } from './database.ts'; +import { AppConfig } from './config.ts'; + +const app = new App({ + config: AppConfig, + providers: [MainDatabase], + imports: [ + new FrameworkModule({ + migrateOnStartup: true, + debug: true, + }) + ] +}); +app.loadConfigFromEnv({prefix: 'APP_', namingStrategy: 'upper', envFilePath: ['local.env', 'prod.env']}); +app.run(); +``` + +现在,由于我们使用了 loadConfigFromEnv,我们可以通过环境变量来设置数据库凭据。 + +```sh +APP_DATABASE_HOST=localhost APP_DATABASE_USER=postgres ts-node app.ts server:start +``` + +或者在 `local.env` 文件中设置,并在未预先设置任何环境变量的情况下启动 `ts-node app.ts server:start`。 + +```sh +APP_DATABASE_HOST=localhost +APP_DATABASE_USER=postgres +``` + +## 多个数据库 + +你可以按需添加任意数量的数据库类,并为它们任意命名。请确保修改每个数据库的名称,以免在使用 Deepkit ORM 浏览器时与其他数据库冲突。 + +## 管理数据 + +现在你已经完成所有设置,可以使用 Deepkit ORM 浏览器来管理你的数据库数据。要打开 Deepkit ORM 浏览器并管理内容,请将上述所有步骤写入 `app.ts` 文件并启动服务器。 + +```sh +$ ts-node app.ts server:start +2021-06-11T15:08:54.330Z [LOG] Start HTTP server, using 1 workers. +2021-06-11T15:08:54.333Z [LOG] Migrate database default +2021-06-11T15:08:54.336Z [LOG] RPC DebugController deepkit/debug/controller +2021-06-11T15:08:54.337Z [LOG] RPC OrmBrowserController orm-browser/controller +2021-06-11T15:08:54.337Z [LOG] HTTP OrmBrowserController +2021-06-11T15:08:54.337Z [LOG] GET /_orm-browser/query httpQuery +2021-06-11T15:08:54.337Z [LOG] HTTP StaticController +2021-06-11T15:08:54.337Z [LOG] GET /_debug/:any serviceApp +2021-06-11T15:08:54.337Z [LOG] HTTP listening at http://localhost:8080/ +``` + +现在你可以打开 http://localhost:8080/_debug/database/default。 + +![调试器数据库](/assets/documentation/framework/debugger-database.png) + +你可以看到 ER(实体关系)图。目前只有一个实体可用。如果你添加了更多带关系的实体,你将一目了然地看到所有信息。 + +如果你在左侧侧栏点击 `User`,即可管理其内容。点击 `+` 图标并修改新记录的标题。更改所需的值(例如用户名)后,点击 `Confirm`。这会将所有更改提交到数据库并使之生效。自增 ID 会自动分配。 + +![调试器数据库用户](/assets/documentation/framework/debugger-database-user.png) + +## 进一步了解 + +要进一步了解 `SQLiteDatabase` 的工作方式,请阅读[数据库](../orm.md)一章及其子章节,例如查询数据、通过会话操作数据、定义关系等。 +请注意,那些章节针对的是独立库 `@deepkit/orm`,并不包含本章上文所述的 Deepkit 框架部分的文档。在独立库中,你需要手动实例化数据库类,例如通过 `new SQLiteDatabase()`。然而,在你的 Deepkit 应用中,这由依赖注入容器自动完成。 + +## 迁移 + +Deepkit 框架提供了强大的迁移系统,允许你创建、执行和回滚迁移。该迁移系统基于 Deepkit ORM 库,因此与框架完美集成。 + +`FrameworkModule` 提供了多个用于管理迁移的命令。 + +- `migration:create` - 基于数据库差异生成新的迁移文件 +- `migration:pending` - 显示待执行的迁移文件 +- `migration:up` - 执行待执行的迁移文件 +- `migration:down` - 执行向下迁移,回滚旧的迁移文件 + +```sh +ts-node app.ts migration:create --migrationDir src/migrations +``` + +一个新的迁移文件会创建在 `migrations` 目录中。该目录是 FrameworkModule 中配置的默认目录。要更改它,可以通过环境变量(如[配置](configuration.md)一章所述)修改配置,或在构造 `FrameworkModule` 时传入 `migrationDir` 选项。 + +```typescript +new FrameworkModule({ + migrationDir: 'src/migrations', +}) +``` + +新创建的迁移文件现在包含 up 和 down 方法,它们基于你的 TypeScript 应用中定义的实体与已配置数据库之间的差异生成。 +你可以根据需要修改 up 方法。down 方法会基于 up 方法自动生成。 +将该文件提交到你的代码仓库,以便其他开发者也能执行它。 + +### 待执行的迁移 + +```sh +ts-node app.ts migration:pending --migrationDir src/migrations +``` + +这会显示所有待执行的迁移。如果你有尚未执行的新迁移文件,它会列在这里。 + +### 执行迁移 + +```sh +ts-node app.ts migration:up --migrationDir src/migrations +``` + +这将执行下一个待执行的迁移。 + +### 回滚迁移 + +```sh +ts-node app.ts migration:down --migrationDir src/migrations +``` + +这将回滚上一次执行的迁移。 + +### 伪迁移 + +假设你尝试执行一次迁移(向上或向下),但失败了。你手动修复了问题,但现在无法再次执行该迁移,因为它已被标记为已执行。此时可以使用 `--fake` 选项来伪装执行:在不实际执行的情况下,将该迁移在数据库中标记为已执行。这样你就可以继续执行下一个待执行的迁移。 + +```sh +ts-node app.ts migration:up --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/framework/deployment.md b/website/src/translations/zh/documentation/framework/deployment.md new file mode 100644 index 000000000..a94d566f1 --- /dev/null +++ b/website/src/translations/zh/documentation/framework/deployment.md @@ -0,0 +1,138 @@ +# 部署 + +在本章中,你将学习如何将应用编译为 JavaScript,为生产环境进行配置,并使用 Docker 进行部署。 + +## 编译 TypeScript + +假设你在 `app.ts` 文件中有如下应用: + +```typescript +#!/usr/bin/env ts-node-script +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class Config { + title: string = 'DEV my Page'; +} + +class MyWebsite { + constructor(protected title: Config['title']) { + } + + @http.GET() + helloWorld() { + return 'Hello from ' + this.title; + } +} + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + .loadConfigFromEnv() + .run(); +``` + +如果你使用 `ts-node app.ts server:start`,你会发现一切正常工作。在生产环境中,你通常不会使用 `ts-node` 启动服务器。你会先将其编译为 JavaScript,然后使用 Node 运行它。为此,你必须有一个正确的 `tsconfig.json` 并设置正确的配置选项。在“第一个应用”一节中,你的 `tsconfig.json` 被配置为将 JavaScript 输出到 `.dist` 文件夹。我们假定你也做了相同的配置。 + +如果所有编译器设置都正确,并且你的 `outDir` 指向类似 `dist` 的文件夹,那么一旦在项目中运行 `tsc` 命令,`tsconfig.json` 中列出的所有文件都会被编译为 JavaScript。只需在该列表中指定你的入口文件即可。所有被导入的文件也会被自动编译,不需要显式添加到 `tsconfig.json` 中。`tsc` 是安装 `npm install typescript` 时 TypeScript 的一部分。 + +```sh +$ ./node_modules/.bin/tsc +``` + +如果成功,TypeScript 编译器不会输出任何内容。你现在可以检查 `dist` 的输出。 + +```sh +$ tree dist +dist +└── app.js +``` + +你可以看到只有一个文件。你可以通过 `node distapp.js` 运行它,并获得与 `ts-node app.ts` 相同的功能。 + +对于部署,关键是 TypeScript 文件被正确编译,并且一切都能直接通过 Node 正常运行。你现在可以直接移动 `dist` 文件夹以及其中的 `node_modules`,然后运行 `node distapp.js server:start`,你的应用就成功部署了。不过,你更可能使用 Docker 之类的其他方案来正确打包你的应用。 + +## 配置 + +在生产环境中,你通常不会将服务器绑定到 `localhost`,而是更可能通过 `0.0.0.0` 绑定到所有设备。如果你不在反向代理之后,你还会将端口设置为 80。要配置这两个设置,你需要自定义 `FrameworkModule`。我们关注的两个选项是 `host` 和 `port`。为了让它们可以通过环境变量或 .dotenv 文件在外部进行配置,我们必须先允许这样做。幸运的是,上面的代码已经通过 `loadConfigFromEnv()` 方法完成了这一步。 + +请参考[配置](../app/configuration.md)章节,了解更多关于如何设置应用配置选项的信息。 + +要查看可用的配置选项以及它们的值,你可以使用 `ts-node app.ts app:config` 命令。你也可以在框架调试器(Framework Debugger)中查看它们。 + +### SSL + +建议(有时是必须)使用启用 SSL 的 HTTPS 来运行你的应用。配置 SSL 有多种选项。要启用 SSL,请使用 +`framework.ssl`,并通过以下选项配置其参数。 + +|=== +|名称|类型|描述 + +|framework.ssl|boolean|为 true 时启用 HTTPS 服务器 +|framework.httpsPort|number?|如果同时定义了 httpsPort 和 ssl,则会在 http 服务器之外额外启动 https 服务器。 +|framework.sslKey|string?|用于 https 的 SSL key 文件路径 +|framework.sslCertificate|string?|用于 https 的证书文件路径 +|framework.sslCa|string?|用于 https 的 CA 文件路径 +|framework.sslCrl|string?|用于 https 的 CRL 文件路径 +|framework.sslOptions|object?|与 tls.SecureContextOptions 和 tls.TlsOptions 相同的接口。 +|=== + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// 在此处编写你的配置和 HTTP 控制器 + +new App({ + config: Config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + sslKey: __dirname + 'path/ssl.key', + sslCertificate: __dirname + 'path/ssl.cert', + sslCA: __dirname + 'path/ssl.ca', + }) + ] +}) + .run(); +``` + +### 本地 SSL + +在本地开发环境中,可通过 `framework.selfSigned` 选项启用自签名 HTTPS。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// 在此处编写你的配置和 HTTP 控制器 + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + ssl: true, + selfSigned: true, + }) + ] +}) + .run(); +``` + +```sh +$ ts-node app.ts server:start +2021-06-13T18:04:01.563Z [LOG] Start HTTP server, using 1 workers. +2021-06-13T18:04:01.598Z [LOG] Self signed certificate for localhost created at var/self-signed-localhost.cert +2021-06-13T18:04:01.598Z [LOG] Tip: If you want to open this server via chrome for localhost, use chrome://flags/#allow-insecure-localhost +2021-06-13T18:04:01.606Z [LOG] HTTP MyWebsite +2021-06-13T18:04:01.606Z [LOG] GET / helloWorld +2021-06-13T18:04:01.606Z [LOG] HTTPS listening at https://localhost:8080/ +``` + +如果你现在启动这个服务器,你的 HTTP 服务器将通过 `https:localhost:8080` 以 HTTPS 访问。在 Chrome 中,当你打开该 URL 时会看到错误信息“NET::ERR_CERT_INVALID”,因为自签名证书被视为安全风险:`chrome:flagsallow-insecure-localhost`。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/framework/public.md b/website/src/translations/zh/documentation/framework/public.md new file mode 100644 index 000000000..952791fd2 --- /dev/null +++ b/website/src/translations/zh/documentation/framework/public.md @@ -0,0 +1,34 @@ +# 公共目录 + +`FrameworkModule` 提供了一种通过 HTTP 提供静态文件(如图片、PDF、二进制文件等)的方法。`publicDir` 配置选项允许你指定用于处理未通往 HTTP 控制器路由的请求的默认入口点的文件夹。默认情况下,此行为是禁用的(空值)。 + +要启用公共文件的提供功能,请将 `publicDir` 设置为你选择的文件夹。通常你会选择像 `publicDir` 这样的名称以便一目了然。 + +``` +. +├── app.ts +└── publicDir + └── logo.jpg +``` + +要更改 `publicDir` 选项,你可以修改 `FrameworkModule` 的第一个参数。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; + +// 在此处添加你的配置和 HTTP 控制器 + +new App({ + config: config, + controllers: [MyWebsite], + imports: [ + new FrameworkModule({ + publicDir: 'publicDir' + }) + ] +}) + .run(); +``` + +现在,该配置的文件夹中的所有文件都可以通过 HTTP 访问。例如,如果你打开 `http:localhost:8080/logo.jpg`,你将看到位于 `publicDir` 目录中的图片 `logo.jpg`。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/framework/testing.md b/website/src/translations/zh/documentation/framework/testing.md new file mode 100644 index 000000000..aa7f36467 --- /dev/null +++ b/website/src/translations/zh/documentation/framework/testing.md @@ -0,0 +1,179 @@ +# 测试 + +Deepkit 框架中的服务和控制器旨在支持 SOLID 和简洁的代码,具有良好的设计、封装和分离。这些特性使代码易于测试。 + +本文档将向你展示如何使用 `ts-jest` 设置名为 [Jest](https://jestjs.io) 的测试框架。为此,运行以下命令安装 `jest` 和 `ts-jest`。 + +```sh +npm install jest ts-jest @types/jest +``` + +Jest 需要一些配置选项来知道从哪里查找测试套件以及如何编译 TS 代码。将以下配置添加到你的 `package.json`: + +```json title=package.json +{ + ..., + + "jest": { + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "testEnvironment": "node", + "testMatch": [ + "**/*.spec.ts" + ] + } +} +``` + +你的测试文件应命名为 `.spec.ts`。创建一个 `test.spec.ts` 文件,并写入以下内容。 + +```typescript +test('first test', () => { + expect(1 + 1).toBe(2); +}); +``` + +现在你可以使用 jest 命令一次性运行所有测试套件。 + +```sh +$ node_modules/.bin/jest + PASS ./test.spec.ts + ✓ first test (1 ms) + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: 0.23 s, estimated 1 s +Ran all test suites. +``` + +请阅读 [Jest-Dokumentation](https://jestjs.io) 以了解更多关于 Jest CLI 工具的工作方式,以及如何编写更复杂的测试和完整的测试套件。 + +## 单元测试 + +在可能的情况下,你应该为你的服务编写单元测试。你的服务依赖越简单、分离越好、定义越清晰,就越容易对其进行测试。在这种情况下,你可以编写如下简单测试: + +```typescript +export class MyService { + helloWorld() { + return 'hello world'; + } +} +``` + +```typescript +// +import { MyService } from './my-service.ts'; + +test('hello world', () => { + const myService = new MyService(); + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +## 集成测试 + +并非总是可以编写单元测试,也并非总是用单元测试覆盖业务关键代码和行为的最高效方式。特别是当你的架构非常复杂时,能够轻松地执行端到端的集成测试将非常有益。 + +正如你在依赖注入章节中已经了解到的那样,依赖注入容器是 Deepkit 的核心。所有服务都在这里构建并运行。你的应用定义了服务(providers)、控制器、监听器和 imports。对于集成测试,你不一定希望在一个测试用例中拥有所有服务,但通常希望有一个精简版的应用来测试关键区域。 + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { http, HttpRequest } from '@deepkit/http'; + +test('http controller', async () => { + class MyController { + + @http.GET() + hello(@http.query() text: string) { + return 'hello ' + text; + } + } + + const testing = createTestingApp({ controllers: [MyController] }); + await testing.startServer(); + + const response = await testing.request(HttpRequest.GET('/').query({text: 'world'})); + + expect(response.getHeader('content-type')).toBe('text/plain; charset=utf-8'); + expect(response.body.toString()).toBe('hello world'); +}); +``` + +```typescript +import { createTestingApp } from '@deepkit/framework'; + +test('service', async () => { + class MyService { + helloWorld() { + return 'hello world'; + } + } + + const testing = createTestingApp({ providers: [MyService] }); + + // 访问依赖注入容器并实例化 MyService + const myService = testing.app.get(MyService); + + expect(myService.helloWorld()).toBe('hello world'); +}); +``` + +如果你已将应用拆分为多个模块,你可以更轻松地测试它们。例如,假设你创建了一个 `AppCoreModule` 并希望测试一些服务。 + +```typescript +class Config { + items: number = 10; +} + +export class MyService { + constructor(protected items: Config['items']) { + + } + + doIt(): boolean { + // 做点什么 + return true; + } +} + +export AppCoreModule = new AppModule({}, { + config: config, + provides: [MyService] +}, 'core'); +``` + +你可以如下使用你的模块: + +```typescript +import { AppCoreModule } from './app-core.ts'; + +new App({ + imports: [new AppCoreModule] +}).run(); +``` + +并且在不启动整个应用服务器的情况下测试它。 + +```typescript +import { createTestingApp } from '@deepkit/framework'; +import { AppCoreModule, MyService } from './app-core.ts'; + +test('service simple', async () => { + const testing = createTestingApp({ imports: [new AppCoreModule] }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); + +test('service simple big', async () => { + // 针对特定测试场景更改模块的配置 + const testing = createTestingApp({ + imports: [new AppCoreModule({items: 100})] + }); + + const myService = testing.app.get(MyService); + expect(myService.doIt()).toBe(true); +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http.md b/website/src/translations/zh/documentation/http.md new file mode 100644 index 000000000..697e0d114 --- /dev/null +++ b/website/src/translations/zh/documentation/http.md @@ -0,0 +1,69 @@ +# HTTP + +处理 HTTP 请求是服务器最为人所知的任务之一。它将输入(HTTP 请求)转换为输出(HTTP 响应)并执行特定任务。客户端可以通过多种方式在 HTTP 请求中向服务器发送数据,服务器必须正确读取并处理这些数据。除了 HTTP body,还可以通过 HTTP query 或 HTTP header 传递值。数据实际如何处理取决于服务器,由服务器定义客户端应当将这些值发送到哪里以及以何种方式发送。 + +这里的首要任务不仅是正确执行用户所期望的操作,还要正确转换(反序列化)并验证来自 HTTP 请求的任何输入。 + +HTTP 请求在服务器上经过的流水线可以多种多样且复杂。许多简单的 HTTP 库在给定路由中只传递 HTTP 请求和 HTTP 响应,并期望开发者直接处理 HTTP 响应。中间件 API 允许按需扩展这条流水线。 + +_Express 示例_ + +```typescript +const http = express(); +http.get('/user/:id', (request, response) => { + response.send({id: request.params.id, username: 'Peter' ); +}); +``` + +这非常适合简单用例,但随着应用程序的增长会很快变得混乱,因为所有输入和输出都必须手动序列化或反序列化并进行验证。同时,还必须考虑如何从应用本身获取诸如数据库抽象之类的对象和服务。这迫使开发者在其之上构建一层架构来映射这些必需的功能。 + +相反,Deepkit 的 HTTP 库利用了 TypeScript 和依赖注入的力量。基于定义的类型,任何值的序列化/反序列化与验证都会自动进行。它还允许像上面的示例那样通过函数式 API 定义路由,或通过控制器类定义路由,以满足不同架构的需求。 + +它既可以与现有的 HTTP 服务器一起使用(如 Node 的 `http` 模块),也可以与 Deepkit 框架一起使用。两种 API 变体都可以访问依赖注入容器,从而方便地从应用中获取诸如数据库抽象和配置等对象。 + +## 示例:函数式 API + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; + +//函数式 API +const app = new App({ + imports: [new FrameworkModule()] +}); +const router = app.get(HttpRouterRegistry); + +router.get('/user/:id', (id: number & Positive, database: Database) => { + //id 保证为 number 类型且为正数。 + //database 由 DI 容器注入。 + return database.query(User).filter({ id }).findOne(); +}); + +app.run(); +``` + +## 类控制器 API + +```typescript +import { Positive } from '@deepkit/type'; +import { http, HttpRouterRegistry } from '@deepkit/http'; +import { FrameworkModule } from "@deepkit/framework"; +import { User } from "discord.js"; + +//控制器 API +class UserController { + constructor(private database: Database) { + } + + @http.GET('/user/:id') + user(id: number & Positive) { + return this.database.query(User).filter({ id }).findOne(); + } +} + +const app = new App({ + controllers: [UserController], + imports: [new FrameworkModule()] +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http/dependency-injection.md b/website/src/translations/zh/documentation/http/dependency-injection.md new file mode 100644 index 000000000..77852fd6e --- /dev/null +++ b/website/src/translations/zh/documentation/http/dependency-injection.md @@ -0,0 +1,67 @@ +# 依赖注入 + +路由函数、控制器类以及控制器方法都可以定义任意依赖,这些依赖由依赖注入容器解析。例如,可以方便地获取数据库抽象或日志记录器。 + +例如,如果将数据库作为提供者提供,则可以注入: + +```typescript +class Database { + //... +} + +const app = new App({ + providers: [ + Database, + ], +}); +``` + +_函数式 API:_ + +```typescript +router.get('/user/:id', async (id: number, database: Database) => { + return await database.query(User).filter({id}).findOne(); +}); +``` + +_控制器 API:_ + +```typescript +class UserController { + constructor(private database: Database) {} + + @http.GET('/user/:id') + async userDetail(id: number) { + return await this.database.query(User).filter({id}).findOne(); + } +} + +//或者直接在方法中 +class UserController { + @http.GET('/user/:id') + async userDetail(id: number, database: Database) { + return await database.query(User).filter({id}).findOne(); + } +} +``` + +参见[依赖注入](dependency-injection)了解更多。 + +## 作用域 + +所有 HTTP 控制器和函数式路由都在 `http` 依赖注入作用域中进行管理。HTTP 控制器会为每个 HTTP 请求相应实例化。这也意味着两者都可以访问注册在 `http` 作用域中的提供者。因此,来自 `@deepkit/http` 的 `HttpRequest` 和 `HttpResponse` 也可以作为依赖使用。如果使用 deepkit 框架,来自 `@deepkit/framework` 的 `SessionHandler` 也可用。 + +```typescript +import { HttpResponse } from '@deepkit/http'; + +router.get('/user/:id', (id: number, request: HttpRequest) => { +}); + +router.get('/', (response: HttpResponse) => { + response.end('Hello'); +}); +``` + +在 `http` 作用域中放置提供者可能很有用,例如为每个 HTTP 请求实例化服务。一旦该 HTTP 请求处理完毕,`http` 作用域的 DI 容器就会被删除,其所有提供者实例也会由垃圾回收器(GC)清理。 + +参见[依赖注入作用域](dependency-injection.md#di-scopes)了解如何将提供者放入 `http` 作用域。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http/events.md b/website/src/translations/zh/documentation/http/events.md new file mode 100644 index 000000000..c48f934ff --- /dev/null +++ b/website/src/translations/zh/documentation/http/events.md @@ -0,0 +1,84 @@ +# 事件 + +HTTP 模块基于一个工作流引擎,该引擎提供了多种事件令牌,可用于挂接到处理 HTTP 请求的整个流程中。 + +该工作流引擎是一个有限状态机,会为每个 HTTP 请求创建一个新的状态机实例,然后在各个位置之间跳转。第一个位置是 `start`,最后一个是 `response`。在每个位置都可以执行附加代码。 + +![HTTP 工作流](/assets/documentation/framework/http-workflow.png) + +每个事件令牌都有其自己的事件类型,并附带额外信息。 + +| 事件令牌 | 描述 | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------| +| httpWorkflow.onRequest | 当有新请求进来时 | +| httpWorkflow.onRoute | 当需要从请求解析路由时 | +| httpWorkflow.onRouteNotFound | 当未找到路由时 | +| httpWorkflow.onAuth | 当进行身份验证时 | +| httpWorkflow.onResolveParameters | 当解析路由参数时 | +| httpWorkflow.onAccessDenied | 当访问被拒绝时 | +| httpWorkflow.onController | 当调用控制器动作时 | +| httpWorkflow.onControllerError | 当控制器动作抛出错误时 | +| httpWorkflow.onParametersFailed | 当解析路由参数失败时 | +| httpWorkflow.onResponse | 当控制器动作已被调用时。此处将结果转换为响应。 | + +由于所有 HTTP 事件都基于工作流引擎,可以使用指定的事件并通过 `event.next()` 方法跳转,从而修改其行为。 + +HTTP 模块在这些事件令牌上使用了自身的事件监听器来实现 HTTP 请求处理。所有这些事件监听器的优先级为 100,这意味着当你监听某个事件时,你的监听器默认会先执行(因为默认优先级是 0)。将优先级设为高于 100 可在 HTTP 默认处理器之后运行。 + +例如,假设你想拦截控制器被调用时的事件。如果将要调用某个特定控制器,我们检查用户是否有访问权限。若有,则继续;若没有,则跳转到下一个工作流项 `accessDenied`。在那里,访问被拒绝的处理流程会被自动继续执行。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HtmlResponse, http, httpAction, httpWorkflow } from '@deepkit/http'; +import { eventDispatcher } from '@deepkit/event'; + +class MyWebsite { + @http.GET('/') + open() { + return 'Welcome'; + } + + @http.GET('/admin').group('secret') + secret() { + return 'Welcome to the dark side'; + } +} + +const app = new App({ + controllers: [MyWebsite], + imports: [new FrameworkModule] +}) + +app.listen(httpWorkflow.onController, async (event) => { + if (event.route.groups.includes('secret')) { + //在此检查身份验证信息,例如 Cookie 会话、JWT 等。 + + //这会跳转到 'accessDenied' 工作流状态, + // 从而执行所有 onAccessDenied 监听器。 + + //由于我们的监听器在 HTTP 内核监听器之前被调用, + // 标准的控制器动作将不会被调用。 + // 底层等同于调用 event.next('accessDenied', ...) + event.accessDenied(); + } +}); + +/** + * 我们更改默认的 accessDenied 实现。 + */ +app.listen(httpWorkflow.onAccessDenied, async () => { + if (event.sent) return; + if (event.hasNext()) return; + event.send(new HtmlResponse('No access to this area.', 403)); +}) + +app.run(); +``` + +```sh +$ curl http://localhost:8080/ +Welcome +$ curl http://localhost:8080/admin +No access to this area +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http/getting-started.md b/website/src/translations/zh/documentation/http/getting-started.md new file mode 100644 index 000000000..a3c834b44 --- /dev/null +++ b/website/src/translations/zh/documentation/http/getting-started.md @@ -0,0 +1,210 @@ +# 入门 + +由于 Deepkit HTTP 基于运行时类型,因此需要先正确安装运行时类型。参见 [运行时类型安装](../runtime-types/getting-started.md)。 + +如果这一步成功完成,则可以安装 `@deepkit/app`,或者使用已经在底层集成该库的 Deepkit 框架。 + +```sh +npm install @deepkit/http +``` + +请注意,用于控制器 API 的 `@deepkit/http` 基于 TypeScript 注解,一旦使用控制器 API,就必须通过 `experimentalDecorators` 启用此特性。 +如果你不使用类,则无需启用该特性。 + +_文件:tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +库安装后,可直接使用其 API。 + +## 函数式 API + +函数式 API 基于函数,可通过路由注册表进行注册;该注册表可以从应用的 DI 容器获取。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +const app = new App({ + imports: [new FrameworkModule] +}); + +const router = app.get(HttpRouterRegistry); + +router.get('/', () => { + return "Hello World!"; +}); + +app.run(); +``` + +在使用模块时,函数式路由也可以由模块动态提供。 + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { HttpRouterRegistry } from '@deepkit/http'; + +class MyModule extends createModuleClass({}) { + override process() { + this.configureProvider<HttpRouterRegistry>(router => { + router.get('/', () => { + return "Hello World!"; + }); + }); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +参见 [框架模块](../app/modules),以了解更多关于应用模块的信息。 + +## 控制器 API + +控制器 API 基于类,可通过 App API 的 `controllers` 选项进行注册。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +new App({ + controllers: [MyPage], + imports: [new FrameworkModule] +}).run(); +``` + +在使用模块时,控制器也可以由模块提供。 + +```typescript +import { App, createModuleClass } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http } from '@deepkit/http'; + +class MyPage { + @http.GET('/') + helloWorld() { + return "Hello World!"; + } +} + +class MyModule extends createModuleClass({}) { + override process() { + this.addController(MyPage); + } +} + +const app = new App({ + imports: [new FrameworkModule, new MyModule] +}); +``` + +若要动态提供控制器(例如取决于配置选项),可以使用 `process` 钩子。 + +```typescript +class MyModuleConfiguration { + debug: boolean = false; +} + +class MyModule extends createModuleClass({ + config: MyModuleConfiguration +}) { + override process() { + if (this.config.debug) { + class DebugController { + @http.GET('/debug/') + root() { + return 'Hello Debugger'; + } + } + this.addController(DebugController); + } + } +} +``` + +参见 [框架模块](../app/modules),以了解更多关于应用模块的信息。 + +## HTTP 服务器 + +如果使用 Deepkit 框架,则已内置 HTTP 服务器。不过,该 HTTP 库也可以在不使用 Deepkit 框架的情况下配合自有的 HTTP 服务器使用。 + +```typescript +import { Server } from 'http'; +import { HttpRequest, HttpResponse } from '@deepkit/http'; + +const app = new App({ + controllers: [MyPage], + imports: [new HttpModule] +}); + +const httpKernel = app.get(HttpKernel); + +new Server( + { IncomingMessage: HttpRequest, ServerResponse: HttpResponse, }, + ((req, res) => { + httpKernel.handleRequest(req as HttpRequest, res as HttpResponse); + }) +).listen(8080, () => { + console.log('listen at 8080'); +}); +``` + +## HTTP 客户端 + +待办:fetch API、验证、und 类型转换。 + +## 路由名称 + +可以为路由指定唯一名称,以便在转发时引用。根据所用 API 的不同,命名方式有所差异。 + +```typescript +// 函数式 API +router.get({ + path: '/user/:id', + name: 'userDetail' +}, (id: number) => { + return {userId: id}; +}); + +// 控制器 API +class UserController { + @http.GET('/user/:id').name('userDetail') + userDetail(id: number) { + return {userId: id}; + } +} +``` + +对于所有具有名称的路由,可以通过 `Router.resolveUrl()` 获取其 URL。 + +```typescript +import { HttpRouter } from '@deepkit/http'; +const router = app.get(HttpRouter); +router.resolveUrl('userDetail', {id: 2}); //=> '/user/2' +``` + +## 安全 + +## 会话 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http/input-output.md b/website/src/translations/zh/documentation/http/input-output.md new file mode 100644 index 000000000..91b3f38cc --- /dev/null +++ b/website/src/translations/zh/documentation/http/input-output.md @@ -0,0 +1,484 @@ +# 输入与输出 + +HTTP 路由的输入与输出是指发送到服务器的数据以及返回给客户端的数据。这包括路径参数、查询参数、请求体、请求头以及响应本身。在本章中,我们将介绍如何在 HTTP 路由中读取、反序列化、验证和写入数据。 + +## 输入 + +以下所有输入方式在函数式 API 和控制器 API 中的工作方式相同。它们允许以类型安全且解耦的方式从 HTTP 请求中读取数据。这不仅显著提升了安全性,也简化了单元测试,因为严格来说,测试路由时甚至不需要一个 HTTP 请求对象实际存在。 + +所有参数都会自动转换(反序列化)为定义的 TypeScript 类型并被验证。这通过 Deepkit 运行时类型及其 [序列化](../runtime-types/serialization.md) 和 [验证](../runtime-types/validation) 功能完成。 + +为简单起见,下面的示例均使用函数式 API。 + +### 路径参数 + +路径参数是从路由的 URL 中提取的值。其类型取决于函数或方法中对应参数的类型。转换会通过功能 [软类型转换](../runtime-types/serialization#soft-type-conversion) 自动完成。 + +```typescript +router.get('/:text', (text: string) => { + return 'Hello ' + text; +}); +``` + +```sh +$ curl http://localhost:8080/galaxy +Hello galaxy +``` + +如果路径参数被定义为非 string 类型,它也会被正确转换。 + +```typescript +router.get('/user/:id', (id: number) => { + return `${id} ${typeof id}`; +}); +``` + +```sh +$ curl http://localhost:8080/user/23 +23 number +``` + +还可以对类型应用额外的验证约束。 + +```typescript +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: number & Positive) => { + return `${id} ${typeof id}`; +}); +``` + +可以应用来自 `@deepkit/type` 的所有验证类型。更多内容参见 [HTTP 验证](#validation)。 + +路径参数在 URL 匹配中默认使用 `[^]+` 作为正则表达式。可以如下自定义该正则表达式: + +```typescript +import { HttpRegExp } from '@deepkit/http'; +import { Positive } from '@deepkit/type'; + +router.get('/user/:id', (id: HttpRegExp<number & Positive, '[0-9]+'>) => { + return `${id} ${typeof id}`; +}); +``` + +这只在少数情况下是必要的,因为通常类型与验证类型的组合已经能正确限制可能的取值范围。 + +### 查询参数 + +查询参数是 URL 中 `?` 字符之后的值,可以通过 `HttpQuery<T>` 类型读取。参数名对应查询参数的名称。 + +```typescript +import { HttpQuery } from '@deepkit/http'; + +router.get('/', (text: HttpQuery<number>) => { + return `Hello ${text}`; +}); +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +``` + +查询参数同样会被自动反序列化并验证。 + +```typescript +import { HttpQuery } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/', (text: HttpQuery<string> & MinLength<3>) => { + return 'Hello ' + text; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy +Hello galaxy +$ curl http://localhost:8080/\?text\=ga +error +``` + +可以应用来自 `@deepkit/type` 的所有验证类型。更多内容参见 [HTTP 验证](#validation)。 + +警告:参数值不会被转义/净化。将其直接作为字符串返回为 HTML 会造成安全漏洞(XSS)。务必不要信任外部输入,并在必要时对数据进行过滤/净化/转换。 + +### 查询模型 + +当查询参数数量较多时会很快变得混乱。为此可以使用一个模型(类或接口)来汇总所有可能的查询参数,从而恢复整洁。 + +```typescript +import { HttpQueries } from '@deepkit/http'; + +class HelloWorldQuery { + text!: string; + page: number = 0; +} + +router.get('/', (query: HttpQueries<HelloWorldQuery>) +{ + return 'Hello ' + query.text + ' at page ' + query.page; +} +``` + +```sh +$ curl http://localhost:8080/\?text\=galaxy&page=1 +Hello galaxy at page 1 +``` + +指定模型中的属性可以包含 `@deepkit/type` 支持的所有 TypeScript 类型和验证类型。参见章节 [序列化](../runtime-types/serialization.md) 与 [验证](../runtime-types/validation.md)。 + +### 请求体 + +对于允许 HTTP 请求体的 HTTP 方法,也可以指定一个请求体模型。HTTP 请求的内容类型必须是 `application/x-www-form-urlencoded`、`multipart/form-data` 或 `application/json`,这样 Deepkit 才能自动将其转换为 JavaScript 对象。 + +```typescript +import { HttpBody } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBody<HelloWorldBody>) => { + return 'Hello ' + body.text; +} +``` + +### 请求头 + +### 流 + +### 手动验证处理 + +要手动接管请求体模型的验证,可以使用特殊类型 `HttpBodyValidation<T>`。它允许接收无效的请求体数据,并且可以对错误信息做出非常具体的响应。 + +```typescript +import { HttpBodyValidation } from '@deepkit/type'; + +class HelloWorldBody { + text!: string; +} + +router.post('/', (body: HttpBodyValidation<HelloWorldBody>) => { + if (!body.valid()) { + // 休斯顿,我们遇到了一些错误。 + const textError = body.getErrorMessageForPath('text'); + return 'Text is invalid, please fix it. ' + textError; + } + + return 'Hello ' + body.text; +}) +``` + +一旦 `valid()` 返回 `false`,指定模型中的值可能处于错误状态。这意味着验证失败。如果不使用 `HttpBodyValidation` 且收到不正确的 HTTP 请求,请求将会被直接中止,函数中的代码将永远不会被执行。只有在需要在同一路由中手动处理关于请求体的错误消息时,才使用 `HttpBodyValidation`。 + +指定模型中的属性可以包含 `@deepkit/type` 支持的所有 TypeScript 类型和验证类型。参见章节 [序列化](../runtime-types/serialization.md) 与 [验证](../runtime-types/validation.md)。 + +### 文件上传 + +可以在请求体模型上使用特殊的属性类型来允许客户端上传文件。可以使用任意数量的 `UploadedFile`。 + +```typescript +import { UploadedFile, HttpBody } from '@deepkit/http'; +import { readFileSync } from 'fs'; + +class HelloWordBody { + file!: UploadedFile; +} + +router.post('/', (body: HttpBody<HelloWordBody>) => { + const content = readFileSync(body.file.path); + + return { + uploadedFile: body.file + }; +}) +``` + +```sh +$ curl http://localhost:8080/ -X POST -H "Content-Type: multipart/form-data" -F "file=@Downloads/23931.png" +{ + "uploadedFile": { + "size":6430, + "path":"/var/folders/pn/40jxd3dj0fg957gqv_nhz5dw0000gn/T/upload_dd0c7241133326bf6afddc233e34affa", + "name":"23931.png", + "type":"image/png", + "lastModifiedDate":"2021-06-11T19:19:14.775Z" + } +} +``` + +默认情况下,Router 会将所有上传的文件保存到临时文件夹,并在路由中的代码执行完毕后将其删除。因此有必要读取 `path` 中指定路径的文件,并将其保存到永久位置(本地磁盘、云存储、数据库)。 + +## 验证 + +HTTP 服务器中的验证是必备功能,因为几乎总是在与不可信数据打交道。数据被验证的地方越多,服务器就越稳定。HTTP 路由中的验证可以通过类型和验证约束方便地使用,并由 `@deepkit/type` 的高度优化的验证器进行检查,因此在这方面没有性能问题。因此强烈建议使用这些验证能力。宁可多一次,也不要少一次。 + +所有输入,如路径参数、查询参数和请求体参数,都会自动根据指定的 TypeScript 类型进行验证。如果通过 `@deepkit/type` 的类型指定了额外的约束,这些也会被检查。 + +```typescript +import { HttpQuery, HttpQueries, HttpBody } from '@deepkit/http'; +import { MinLength } from '@deepkit/type'; + +router.get('/:text', (text: string & MinLength<3>) => { + return 'Hello ' + text; +} + +router.get('/', (text: HttpQuery<string> & MinLength<3>) => { + return 'Hello ' + text; +} + +interface MyQuery { + text: string & MinLength<3>; +} + +router.get('/', (query: HttpQueries<MyQuery>) => { + return 'Hello ' + query.text; +}); + +router.post('/', (body: HttpBody<MyQuery>) => { + return 'Hello ' + body.text; +}); +``` + +更多信息参见 [验证](../runtime-types/validation.md)。 + +## 输出 + +路由可以返回多种数据结构。其中一些会被特殊处理,如重定向和模板,另一些如简单对象则会直接以 JSON 发送。 + +### JSON + +默认情况下,普通的 JavaScript 值会以 `applicationjson; charset=utf-8` 头作为 JSON 返回给客户端。 + +```typescript +router.get('/', () => { + // 将以 application/json 发送 + return { hello: 'world' } +}); +``` + +如果为函数或方法指定了显式的返回类型,数据将根据该类型使用 Deepkit JSON 序列化器序列化为 JSON。 + +```typescript +interface ResultType { + hello: string; +} + +router.get('/', (): ResultType => { + // 将以 application/json 发送,且 additionalProperty 会被丢弃 + return { hello: 'world', additionalProperty: 'value' }; +}); +``` + +### HTML + +发送 HTML 有两种方式。可以使用 `HtmlResponse` 对象,或者使用带 JSX 的模板引擎。 + +```typescript +import { HtmlResponse } from '@deepkit/http'; + +router.get('/', () => { + // 将以 Content-Type: text/html 发送 + return new HtmlResponse('<b>Hello World</b>'); +}); +``` + +```typescript +router.get('/', () => { + // 将以 Content-Type: text/html 发送 + return <b>Hello + World < /b>; +}); +``` + +带 JSX 的模板引擎变体的优势在于,使用的变量会被自动进行 HTML 转义。另见 [模板](./template.md)。 + +### 自定义内容类型 + +除了 HTML 和 JSON,还可以以特定的内容类型发送文本或二进制数据。这通过 `Response` 对象完成。 + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('<title>Hello World', 'text/xml'); +}); +``` + +### HTTP 错误 + +通过抛出各种 HTTP 错误,可以立即中断 HTTP 请求的处理,并输出对应错误的 HTTP 状态码。 + +```typescript +import { HttpNotFoundError } from '@deepkit/http'; + +router.get('/user/:id', async (id: number, database: Database) => { + const user = await database.query(User).filter({ id }).findOneOrUndefined(); + if (!user) throw new HttpNotFoundError('User not found'); + return user; +}); +``` + +默认情况下,所有错误都会以 JSON 返回给客户端。该行为可以在事件系统中的事件 `httpWorkflow.onControllerError` 进行自定义。参见 [HTTP 事件](./events.md) 章节。 + +| 错误类 | 状态 | +|---------------------------|------| +| HttpBadRequestError | 400 | +| HttpUnauthorizedError | 401 | +| HttpAccessDeniedError | 403 | +| HttpNotFoundError | 404 | +| HttpMethodNotAllowedError | 405 | +| HttpNotAcceptableError | 406 | +| HttpTimeoutError | 408 | +| HttpConflictError | 409 | +| HttpGoneError | 410 | +| HttpTooManyRequestsError | 429 | +| HttpInternalServerError | 500 | +| HttpNotImplementedError | 501 | + +`HttpAccessDeniedError` 是一个特例。一旦它被抛出,HTTP 工作流(见 [HTTP 事件](./events.md))不会跳转到 `controllerError`,而是跳转到 `accessDenied`。 + +可以通过 `createHttpError` 创建并抛出自定义 HTTP 错误。 + +```typescript +export class HttpMyError extends createHttpError(412, 'My Error Message') { +} +``` + +控制器动作中抛出的错误由 HTTP 工作流事件 `onControllerError` 处理。默认实现是返回包含错误消息和状态码的 JSON 响应。可以通过监听该事件并返回不同的响应来自定义此行为。 + +```typescript +import { httpWorkflow } from '@deepkit/http'; + +new App() + .listen(httpWorkflow.onControllerError, (event) => { + if (event.error instanceof HttpMyError) { + event.send(new Response('My Error Message', 'text/plain').status(500)); + } else { + // 对于所有其他错误,返回通用的错误消息 + event.send(new Response('Something went wrong. Sorry about that.', 'text/plain').status(500)); + } + }) + .listen(httpWorkflow.onAccessDenied, (event) => { + event.send(new Response('Access denied. Try to login first.', 'text/plain').status(403)); + }); +``` + +### 附加响应头 + +要修改 HTTP 响应的头部,可以在 `Response`、`JSONResponse` 和 `HTMLResponse` 对象上调用附加方法。 + +```typescript +import { Response } from '@deepkit/http'; + +router.get('/', () => { + return new Response('Access Denied', 'text/plain') + .header('X-Reason', 'unknown') + .status(403); +}); +``` + +### 重定向 + +要返回 301 或 302 重定向作为响应,可以使用 `Redirect.toRoute` 或 `Redirect.toUrl`。 + +```typescript +import { Redirect } from '@deepkit/http'; + +router.get({ path: '/', name: 'homepage' }, () => { + return Hello + World < /b>; +}); + +router.get({ path: '/registration/complete' }, () => { + return Redirect.toRoute('homepage'); +}); +``` + +`Redirect.toRoute` 方法会使用路由名称。如何设置路由名称可见 [HTTP 路由名称](./getting-started.md#route-names) 一节。如果被引用的路由(查询或路径)包含参数,可以通过第二个参数指定: + +```typescript +router.get({ path: '/user/:id', name: 'user_detail' }, (id: number) => { + +}); + +router.post('/user', (user: HttpBody) => { + //... 保存用户并重定向到其详情页 + return Redirect.toRoute('user_detail', { id: 23 }); +}); +``` + +或者,可以使用 `Redirect.toUrl` 重定向到某个 URL。 + +```typescript +router.post('/user', (user: HttpBody) => { + //... 保存用户并重定向到其详情页 + return Redirect.toUrl('/user/' + 23); +}); +``` + +默认情况下,两者都使用 302 重定向。可以通过 `statusCode` 参数进行自定义。 + +## 解析器 + +Router 支持一种解析复杂参数类型的方式。例如,对于像 `/user/:id` 这样的路由,可以使用解析器在路由之外将该 `id` 解析为一个 `user` 对象。这进一步解耦了 HTTP 抽象与路由代码,并进一步简化了测试和模块化。 + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { http, RouteParameterResolverContext, RouteParameterResolver } from '@deepkit/http'; + +class UserResolver implements RouteParameterResolver { + constructor(protected database: Database) { + } + + async resolve(context: RouteParameterResolverContext) { + if (!context.parameters.id) throw new Error('No :id given'); + return await this.database.getUser(parseInt(context.parameters.id, 10)); + } +} + +@http.resolveParameter(User, UserResolver) +class MyWebsite { + @http.GET('/user/:id') + getUser(user: User) { + return 'Hello ' + user.username; + } +} + +new App({ + controllers: [MyWebsite], + providers: [UserDatabase, UserResolver], + imports: [new FrameworkModule] +}) + .run(); +``` + +`@http.resolveParameter` 中的装饰器指定了要使用 `UserResolver` 解析哪个类。一旦在函数或方法中将指定的类 `User` 作为参数,解析器就会被用来提供该对象。 + +如果在类上指定了 `@http.resolveParameter`,则该类的所有方法都会获得该解析器。装饰器也可以按方法应用: + +```typescript +class MyWebsite { + @http.GET('/user/:id').resolveParameter(User, UserResolver) + getUser(user: User) { + return 'Hello ' + user.username; + } +} +``` + +同样,也可以使用函数式 API: + +```typescript + +router.add( + http.GET('/user/:id').resolveParameter(User, UserResolver), + (user: User) => { + return 'Hello ' + user.username; + } +); +``` + +`User` 对象不一定必须依赖于某个参数。它也可以依赖于会话或某个 HTTP 头部,并且只在用户已登录时才提供。在 `RouteParameterResolverContext` 中可以获取到大量关于 HTTP 请求的信息,从而可以覆盖许多用例。 + +原则上,也可以通过来自 `http` 作用域的依赖注入容器提供复杂的参数类型,因为它们也可用于路由函数或方法中。然而,这样做的缺点是不能使用异步函数调用,因为 DI 容器整体是同步的。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http/middleware.md b/website/src/translations/zh/documentation/http/middleware.md new file mode 100644 index 000000000..62cfb5bb1 --- /dev/null +++ b/website/src/translations/zh/documentation/http/middleware.md @@ -0,0 +1,260 @@ +# 中间件 + +HTTP 中间件允许你在请求/响应周期中挂钩,作为 HTTP 事件的替代方案。其 API 允许你使用来自 Express/Connect 框架的所有中间件。 + +中间件可以是一个类(由依赖注入容器实例化),也可以是一个简单函数。 + +```typescript +import { HttpMiddleware, httpMiddleware, HttpRequest, HttpResponse } from '@deepkit/http'; + +class MyMiddleware implements HttpMiddleware { + async execute(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); + } +} + + +function myMiddlewareFunction(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) { + response.setHeader('middleware', '1'); + next(); +} + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware), + httpMiddleware.for(myMiddlewareFunction), + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 全局 + +通过使用 httpMiddleware.for(MyMiddleware),可以将中间件全局注册到所有路由。 + +```typescript +import { httpMiddleware } from '@deepkit/http'; + +new App({ + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 按控制器 + +可以通过两种方式将中间件限定到一个或多个控制器:使用 `@http.controller`,或使用 `httpMiddleware.for(T).forControllers()`。`excludeControllers` 允许你排除某些控制器。 + +```typescript +@http.middleware(MyMiddleware) +class MyFirstController { + +} +new App({ + providers: [MyMiddleware], + controllers: [MainController, UsersCommand], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyFirstController, MySecondController) + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 按路由名称 + +`forRouteNames` 及其对应的 `excludeRouteNames` 允许你按路由名称筛选中间件的执行。 + +```typescript +class MyFirstController { + @http.GET('/hello').name('firstRoute') + myAction() { + } + + @http.GET('/second').name('secondRoute') + myAction2() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRouteNames('firstRoute', 'secondRoute') + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 按操作/路由 + +若只在某个路由上执行中间件,可以使用 `@http.GET().middleware()`,或使用 `httpMiddleware.for(T).forRoute()`,其中 forRoute 提供多种选项来筛选路由。 + +```typescript +class MyFirstController { + @http.GET('/hello').middleware(MyMiddleware) + myAction() { + } +} +new App({ + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' + }) + ], + imports: [new FrameworkModule] +}).run(); +``` + +`forRoutes()` 的第一个参数支持多种方式来筛选路由。 + +```typescript +{ + path?: string; + pathRegExp?: RegExp; + httpMethod?: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS' | 'TRACE'; + category?: string; + excludeCategory?: string; + group?: string; + excludeGroup?: string; +} +``` + +## 路径模式 + +`path` 支持通配符 *。 + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + path: 'api/*' +}) +``` + +## 正则表达式 + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + pathRegExp: /'api/.*'/ +}) +``` + +## HTTP 方法 + +按某个 HTTP 方法筛选所有路由。 + +```typescript +httpMiddleware.for(MyMiddleware).forRoutes({ + httpMethod: 'GET' +}) +``` + +## 分类 + +`category` 及其对应的 `excludeCategory` 允许你按路由分类进行筛选。 + +```typescript +@http.category('myCategory') +class MyFirstController { + +} + +class MySecondController { + @http.GET().category('myCategory') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + category: 'myCategory' +}) +``` + +## 分组 + +`group` 及其对应的 `excludeGroup` 允许你按路由分组进行筛选。 + +```typescript +@http.group('myGroup') +class MyFirstController { + +} + +class MySecondController { + @http.GET().group('myGroup') + myAction() { + } +} +httpMiddleware.for(MyMiddleware).forRoutes({ + group: 'myGroup' +}) +``` + +## 按模块 + +可以将中间件的执行限定为整个模块。 + +```typescript +httpMiddleware.for(MyMiddleware).forModule(ApiModule) +``` + +## 按自身模块 + +若要让中间件只在其注册所在的模块中的所有控制器/路由上执行,请使用 `forSelfModules()`。 + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //对同一模块中注册的所有控制器 + httpMiddleware.for(MyMiddleware).forSelfModules(), + ], +}); +``` + +## 超时 + +所有中间件最终都需要调用 `next()`。如果某个中间件在超时时间内没有调用 `next()`,则会记录一条警告并继续执行下一个中间件。要将默认的 4 秒更改为其他值,请使用 timeout(milliseconds)。 + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MainController, UsersCommand], + providers: [MyMiddleware], + middlewares: [ + //对同一模块中注册的所有控制器 + httpMiddleware.for(MyMiddleware).timeout(15_000), + ], +}); +``` + +## 多重规则 + +要组合多个筛选条件,可以链式调用方法。 + +```typescript +const ApiModule = new AppModule({}, { + controllers: [MyController], + providers: [MyMiddleware], + middlewares: [ + httpMiddleware.for(MyMiddleware).forControllers(MyController).excludeRouteNames('secondRoute') + ], +}); +``` + +## Express 中间件 + +几乎所有 Express 中间件都受支持。那些依赖于 Express 特定请求方法的中间件目前尚不支持。 + +```typescript +import * as compression from 'compression'; + +const ApiModule = new AppModule({}, { + middlewares: [ + httpMiddleware.for(compress()).forControllers(MyController) + ], +}); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http/security.md b/website/src/translations/zh/documentation/http/security.md new file mode 100644 index 000000000..7cfa8604a --- /dev/null +++ b/website/src/translations/zh/documentation/http/security.md @@ -0,0 +1,3 @@ +# 安全 + +本节仍在建设中。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/http/views.md b/website/src/translations/zh/documentation/http/views.md new file mode 100644 index 000000000..a9f6fa9cc --- /dev/null +++ b/website/src/translations/zh/documentation/http/views.md @@ -0,0 +1,31 @@ +# HTML 视图 + +Deepkit HTTP 自带一个内置的 HTML 视图渲染系统。它基于 JSX,并允许你用 TypeScript 编写视图。它不是拥有自己语法的模板引擎,而是一个功能完备的 TypeScript/JSX 渲染器。 + +它会在运行时优化 JSX 代码并缓存结果。因此速度非常快,几乎没有额外开销。 + + +## JSX + +JSX 是 JavaScript 的语法扩展,并且开箱即用地支持 TypeScript。它允许你在 TypeScript 中编写 HTML。它与 Vue.js 或 React.js 非常相似。 + +```tsx app=app.ts +import { App } from '@deepkit/app'; +import { HttpRouterRegistry } from "@deepkit/http"; + +export function View() { + return
+

Hello World

+

My first JSX view

+
; +} + +const app = new App({}); +const router = app.get(HttpRouterRegistry); + +router.get('/', () => ); + +app.run(); +``` + +```sh \ No newline at end of file diff --git a/website/src/translations/zh/documentation/index.md b/website/src/translations/zh/documentation/index.md new file mode 100644 index 000000000..0511905f5 --- /dev/null +++ b/website/src/translations/zh/documentation/index.md @@ -0,0 +1,87 @@ +# 文档 + +Deepkit 是一个面向后端应用的开源 TypeScript 框架,在 MIT 许可下免费提供,旨在帮助你构建可扩展且易维护的后端应用。它被设计用于在浏览器和 Node.js 中工作,但也可以在任何合适的 JavaScript 环境中运行。 + +在这里你可以找到 Deepkit 各个组件的章节以及我们所有包的 API 参考。 + +如果你需要帮助,欢迎加入我们的[Discord 服务器](https://discord.com/invite/PtfVf7B8UU)或在 [GitHub](https://github.com/deepkit/deepkit-framework) 上提交 issue。 + +## 章节 + + +- [应用](/documentation/app.md) - 基于命令行界面,使用 Deepkit 编写你的第一个应用。 +- [框架](/documentation/framework.md) - 为你的应用添加应用(HTTP/RPC)服务器、API 文档、调试器、集成测试等。 +- [运行时类型](/documentation/runtime-types.md) - 了解 TypeScript 运行时类型,以及如何验证与转换数据。 +- [依赖注入](/documentation/dependency-injection.md) - 依赖注入容器、控制反转与依赖倒置。 +- [文件系统](/documentation/filesystem.md) - 文件系统抽象,以统一方式处理本地与远程文件系统。 +- [消息代理](/documentation/broker.md) - 消息代理抽象,用于处理分布式二级缓存、发布/订阅、队列、集中式原子锁或键值存储。 +- [HTTP](/documentation/http.md) - HTTP 服务器抽象,用于构建类型安全的端点。 +- [RPC](/documentation/rpc.md) - 远程过程调用抽象,用于连接前端与后端,或连接多个后端服务。 +- [ORM](/documentation/orm.md) - ORM 与 DBAL,以类型安全的方式存储和查询数据。 +- [桌面 UI](/documentation/desktop-ui/getting-started) - 使用 Deepkit 基于 Angular 的 UI 框架构建 GUI 应用。 + +## API 参考 + +以下是所有 Deepkit 包的完整列表及其 API 文档链接。 + +### 组成 + +- [@deepkit/app](/documentation/package/app.md) +- [@deepkit/framework](/documentation/package/framework.md) +- [@deepkit/http](/documentation/package/http.md) +- [@deepkit/angular-ssr](/documentation/package/angular-ssr.md) + +### 基础设施 + +- [@deepkit/rpc](/documentation/package/rpc.md) +- [@deepkit/rpc-tcp](/documentation/package/rpc-tcp.md) +- [@deepkit/broker](/documentation/package/broker.md) +- [@deepkit/broker-redis](/documentation/package/broker-redis.md) + +### 文件系统 + +- [@deepkit/filesystem](/documentation/package/filesystem.md) +- [@deepkit/filesystem-ftp](/documentation/package/filesystem-ftp.md) +- [@deepkit/filesystem-sftp](/documentation/package/filesystem-sftp.md) +- [@deepkit/filesystem-s3](/documentation/package/filesystem-s3.md) +- [@deepkit/filesystem-google](/documentation/package/filesystem-google.md) +- [@deepkit/filesystem-database](/documentation/package/filesystem-database.md) + +### 数据库 + +- [@deepkit/orm](/documentation/package/orm.md) +- [@deepkit/mysql](/documentation/package/mysql.md) +- [@deepkit/postgres](/documentation/package/postgres.md) +- [@deepkit/sqlite](/documentation/package/sqlite.md) +- [@deepkit/mongodb](/documentation/package/mongodb.md) + +### 基础 + +- [@deepkit/type](/documentation/package/type.md) +- [@deepkit/event](/documentation/package/event.md) +- [@deepkit/injector](/documentation/package/injector.md) +- [@deepkit/template](/documentation/package/template.md) +- [@deepkit/logger](/documentation/package/logger.md) +- [@deepkit/workflow](/documentation/package/workflow.md) +- [@deepkit/stopwatch](/documentation/package/stopwatch.md) + +### 工具 + +- [@deepkit/api-console](/documentation/package/api-console.md) +- [@deepkit/devtool](/documentation/package/devtool.md) +- [@deepkit/desktop-ui](/documentation/package/desktop-ui.md) +- [@deepkit/orm-browser](/documentation/package/orm-browser.md) +- [@deepkit/bench](/documentation/package/bench.md) +- [@deepkit/run](/documentation/package/run.md) + +### 核心 + +- [@deepkit/bson](/documentation/package/bson.md) +- [@deepkit/core](/documentation/package/core.md) +- [@deepkit/topsort](/documentation/package/topsort.md) + +### 运行时 + +- [@deepkit/vite](/documentation/package/vite.md) +- [@deepkit/bun](/documentation/package/bun.md) +- [@deepkit/type-compiler](/documentation/package/type-compiler.md) \ No newline at end of file diff --git a/website/src/translations/zh/documentation/introduction.md b/website/src/translations/zh/documentation/introduction.md new file mode 100644 index 000000000..86f64dd95 --- /dev/null +++ b/website/src/translations/zh/documentation/introduction.md @@ -0,0 +1,45 @@ +# 简介 + +TypeScript 作为 JavaScript 的高度可扩展超集,旨在用于开发更安全、更健壮的应用程序。虽然 JavaScript 拥有庞大的开发者社区和生态系统,但 TypeScript 将静态类型的力量带入了 JavaScript,大幅减少运行时错误,使代码库更易于维护和理解。然而,尽管优势明显,TypeScript 的潜力并未被充分发挥,尤其是在实现复杂的企业级解决方案时。TypeScript 在编译时会擦除类型信息,这在运行时能力上留下了关键空白,迫使人们为了保留类型信息而采用各种不够友好的变通方案。无论是代码生成、功能受限的装饰器,还是像 Zod 这样带有复杂推断步骤的自定义类型构建器,这些方案都繁琐、缓慢且容易出错。这不仅导致开发速度变慢,还使应用程序的健壮性下降,尤其是在大型团队和复杂项目中。 + +Deepkit 应运而生,它彻底革新了 TypeScript 构建复杂而高效软件解决方案的方式。Deepkit 由 TypeScript 编写并为 TypeScript 而设计,不仅在开发阶段提供类型安全,还将 TypeScript 类型系统的优势扩展到运行时。通过在运行时保留类型信息,Deepkit 为一系列新功能打开了大门,包括动态类型计算、数据校验和序列化,这些功能过去的实现往往十分繁琐。 + +虽然 Deepkit 专为高复杂度项目和企业级应用而打造,但其敏捷性和模块化架构同样适用于小型应用。其广泛的库覆盖常见用例,可按项目需求单独使用或组合使用。Deepkit 的目标是在需要时足够灵活,在必要处足够规范,使开发者在短期和长期都能保持高开发速度。 + +## 为什么选择 Deepkit? + +TypeScript 生态中充斥着各类库和工具,几乎为所有可想象的问题提供了解决方案。尽管选择丰富令人受益匪浅,但不同库之间理念、API 和代码质量的不一致,往往导致集成复杂度激增。整合这些异构组件需要额外的抽象层,并常常产生大量胶水代码,迅速失控,成为维护噩梦,严重拖慢开发速度。Deepkit 旨在通过提供一个统一的框架来缓解这些挑战,将几乎每个项目都需要的核心功能整合在一起。其协调一致的库与组件集合被设计为无缝协作,从而弥合分散的 TypeScript 生态中的种种鸿沟。 + +### 经验证的企业级原则 + +Deepkit 从 Java 的 Spring 以及 PHP 的 Laravel 和 Symfony 等久经考验的企业级框架中汲取灵感。这些框架在数十年间证明了其高效与稳健,是无数成功项目的中坚。Deepkit 以一种全新而独特的方式,将类似的企业级设计模式与概念带入 TypeScript 世界,使开发者能够受益于多年的集体智慧。 + +这些模式不仅提供了经验证的应用架构方式,还能促进开发,尤其是在大型团队中。通过运用这些行之有效的方法论,Deepkit 致力于在 TypeScript 领域提供此前难以实现的可靠性与可扩展性。 + +### 敏捷 / 长期表现 + +Deepkit 在设计时充分考虑了敏捷性,提供能加速初期开发、并在长期维护中受益的工具与特性。不同于一些只追求初期速度、却牺牲未来可扩展性的框架,Deepkit 在两者之间取得平衡。其设计模式易于掌握,上手简单;同时也能有效扩展,确保即使项目和团队不断增长,开发速度也不会下降。 + +这种前瞻性的方式使 Deepkit 不仅适用于快速构建 MVP(最小可行产品),也适合复杂且生命周期长的企业级应用。 + +### 开发者体验 + +Deepkit 还非常重视开发者体验。框架提供直观的 API、详尽的文档以及一个互助的社区,旨在帮助开发者专注于解决业务问题,而不是与技术复杂性交战。无论你是在构建小型应用还是大型企业级系统,Deepkit 都为你提供顺畅且富有成就感的开发之旅所需的工具与实践。 + +## 关键特性 + +### 运行时类型 + +Deepkit 的一大亮点是能够在运行时保留类型信息。传统的 TypeScript 框架通常在编译过程中丢弃这些关键信息,使运行时的操作(如数据校验、序列化或依赖注入)变得更加繁琐。Deepkit 的类型编译器独特地支持在运行时进行动态类型计算并读取已有的类型信息。这不仅带来更大的灵活性,还能构建更健壮且类型安全的应用,从而简化复杂系统的开发。 + +### 全面的库套件 + +Deepkit 提供完整的库生态,用于加速应用开发的各个方面。从数据库抽象、CLI(命令行)解析器,到 HTTP 路由器和 RPC 框架,Deepkit 的库为多样化的编程需求提供统一解决方案。所有这些库都具备在运行时利用 TypeScript 类型系统的额外优势,显著减少样板代码并提升代码清晰度。Deepkit 的模块化特性允许开发者按需选用单个库完成特定任务,或使用整套框架构建功能完备、可用于生产的应用。 + +## 高性能与可扩展性 + +随着项目复杂度增长,保持开发速度是一项艰巨挑战。Deepkit 以强调应用经验证的企业级设计模式来直面这一问题,这些模式被证明能在更大的团队与更复杂的代码库中良好扩展。该框架融入了成熟的企业级设计模式,事实证明它们非常适合大型团队和复杂代码库的扩展需求。Deepkit 的方法确保项目不仅在初期阶段保持敏捷与高效,也能贯穿其整个生命周期。其通过尽可能减少样板代码,并以最符合人体工学的方式运用设计模式,从而使团队在长期内保持高生产力。 + +### 同构 TypeScript + +Deepkit 致力于最大化同构 TypeScript 的优势,使同一代码库可以跨多平台复用——无论是前端、后端,甚至移动应用。这样不仅能显著节省时间与成本,还能在各部门之间共享代码,简化招聘并促进更容易的知识传递。Deepkit 充分释放同构 TypeScript 的力量,提供一体化的跨平台开发体验,显著超越传统的双栈方案。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm.md b/website/src/translations/zh/documentation/orm.md new file mode 100644 index 000000000..6936f63c7 --- /dev/null +++ b/website/src/translations/zh/documentation/orm.md @@ -0,0 +1,15 @@ +# Deepkit ORM + +Deepkit ORM 是一个高性能的 TypeScript ORM(对象关系映射器)。它提供了一个简单直观的 API 用于与数据库交互,让你专注于构建应用,而无需担心底层的数据库操作。该 ORM 构建在 Deepkit 的运行时类型系统之上,为进行数据库操作提供了类型安全的环境。 + +## 为什么使用 ORM? + +Deepkit 中的对象关系映射(ORM)为开发者带来多种好处。 + +1. 简化的数据库操作:使用 ORM,开发者可以抽象掉手动编写与执行 SQL 查询的过程,改用更直观的面向对象方式与数据库交互。这简化了诸如查询、插入、更新和删除记录等常见数据库操作。 + +2. 跨数据库兼容性:ORM 通过提供一致的 API,让开发者可以编写与数据库无关的代码。这意味着你可以在不同的数据库引擎(如 MySQL、PostgreSQL 或 SQLite)之间轻松切换,而无需对代码库做出重大更改。 + +3. 类型安全与编译期检查:通过利用运行时类型信息,Deepkit 的 ORM 为数据库操作提供了类型安全的环境。借助 ORM,你可以将数据库模式定义为 TypeScript 类或接口,从而在编译期而非运行时捕获潜在错误。此外,ORM 会处理自动类型转换与校验,确保你的数据始终一致并被正确持久化到数据库中。 + +总的来说,在 Deepkit 中使用 ORM 可以简化数据库操作、提升跨数据库兼容性,并提供类型安全与编译期检查,使其成为构建健壮且可维护应用程序的关键组件。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/composite-primary-key.md b/website/src/translations/zh/documentation/orm/composite-primary-key.md new file mode 100644 index 000000000..a6c25d9ae --- /dev/null +++ b/website/src/translations/zh/documentation/orm/composite-primary-key.md @@ -0,0 +1,84 @@ +# 复合主键 + +复合主键意味着,一个实体有多个主键,它们会自动组合成一个“复合主键”。这种对数据库的建模方式有优点也有缺点。我们认为复合主键在实践中存在巨大的劣势,且这些劣势不足以支撑其优点,因此应被视为不良实践并尽量避免。Deepkit ORM 不支持复合主键。本章解释其中原因并展示(更好的)替代方案。 + +## 缺点 + +连接并非易事。尽管 RDBMS 中的连接经过了高度优化,但它们在应用中会引入一种始终存在的复杂度,容易失控并导致性能问题。这里的性能不仅包括查询执行时间,也包括开发时间。 + +## 连接 + +每一个连接在涉及的字段越多时就会越复杂。尽管许多数据库实现了优化,使得多字段连接本身不一定更慢,但这要求开发者持续、细致地思考这些连接的细节,因为一旦遗忘某个键,就可能导致微妙的错误(因为即使未指定所有键,连接仍然会执行),因此开发者需要了解完整的复合主键结构。 + +## Indizes + +包含多个字段的索引(即复合主键)在查询时会遭遇字段顺序的问题。尽管数据库系统可以优化某些查询,但复杂的结构会使得编写能正确利用所有已定义索引的高效操作变得困难。对于一个多字段索引(例如复合主键),通常需要按正确的顺序定义字段,数据库才会实际使用该索引。如果顺序指定不正确(例如在 WHERE 子句中),很容易导致数据库完全不使用该索引,而是执行全表扫描。知道数据库会以何种方式优化哪些查询是一种进阶知识——新开发者通常并不具备——但一旦开始使用复合主键,这种知识就变得必要,以便你能最大化利用数据库、避免浪费资源。 + +## Migrationen + +一旦你决定某个实体需要新增一个字段来唯一标识它(从而成为复合主键的一部分),这将导致数据库中所有与该实体存在关系的实体都需要调整。 + +例如,假设你有一个带复合主键的实体 `user`,并决定在不同的表中引用这个 `user` 的外键,例如在中间表 `audit_log`、`groups` 和 `posts` 中。一旦你更改了 `user` 的主键,这些表在迁移时同样都需要调整。 + +这不仅会让迁移文件复杂得多,还可能在执行迁移时造成较长停机时间,因为模式变更通常需要整个数据库锁或至少表级锁。受诸如索引变更这类重大变更影响的表越多,迁移所需时间越长。而表越大,迁移时间也越长。 +以 `audit_log` 表为例。这类表通常有大量记录(数百万级),而你仅仅因为决定使用复合主键并给 `user` 的主键新增一个字段,就不得不在模式变更期间触碰它们。根据这些表的大小,这要么让迁移成本不必要地增加,要么在某些情况下成本高到让更改 `User` 的主键不再具有经济上的合理性。这通常会引出各种变通方案(例如在 user 表上额外增加唯一索引),从而引入技术债,并迟早被列入遗留清单。 + +对于大型项目,这可能导致巨大的停机时间(从几分钟到数小时),甚至引入全新的迁移抽象系统:本质上是复制表、将记录插入幽灵表,并在迁移后来回切换表。所有这些附加复杂度都会施加到任何与具有复合主键的实体存在关系的实体上,并随着数据库结构变大而愈发严峻。问题只会恶化,且无解(除非彻底移除复合主键)。 + +## 可发现性 + +如果你是数据库管理员或数据工程师/科学家,你通常会直接在数据库上工作,并在需要时探索数据。使用复合主键时,任何直接编写 SQL 的人都必须知道所有相关表的正确主键(以及为了获得正确索引优化所需的列顺序)。这种额外的负担不仅会使数据探索、报表生成等复杂化,还可能在复合主键突然发生变化时导致旧 SQL 出错。旧 SQL 可能仍然语法有效、运行正常,但会因为连接中缺少复合主键中新字段而突然返回不正确的结果。只有一个主键要容易得多。这使得查找数据更简单,并确保当你决定改变(例如)用户对象的唯一标识方式时,旧的 SQL 查询仍能正确工作。 + +## 重构 + +一旦实体使用了复合主键,重构该键可能会引发大量额外的重构工作。由于拥有复合主键的实体通常没有单一的唯一字段,所有过滤器和关联都必须包含复合键的全部值。这通常意味着代码依赖于对复合主键的了解,因此必须取回所有这些字段(例如用于类似 user:key1:key2 的 URL)。一旦该键发生变化,所有显式使用这类知识的地方,比如 URL、自定义 SQL 查询等,都必须重写。 + +虽然 ORM 通常能自动创建连接而无需手动指定这些值,但它无法自动覆盖所有其他用例的重构,比如 URL 结构或自定义 SQL 查询,尤其是那些根本未使用 ORM 的地方,例如报表系统和各类外部系统。 + +## ORM 复杂性 + +一旦支持复合主键,像 Deepkit ORM 这样的强大 ORM 的代码复杂性会大幅增加。不仅代码和维护会更复杂、因而更昂贵,用户带来的边界情况也会更多,需要修复和维护。查询层、变更检测、迁移系统、内部关系跟踪等的复杂性都会显著上升。综合考虑,构建并支持一个带复合主键的 ORM 的总体成本过高,难以被合理化,因此 Deepkit 不支持它。 + +## 优点 + +除此之外,复合主键也有一些优点,尽管非常表面。通过为每张表尽可能少地使用索引,写入(插入/更新)数据会更高效,因为需要维护的索引更少。它还会让模型结构稍微更简洁一些(因为通常会少一列)。然而,如今顺序的、自动递增的主键与非递增主键之间的差异几乎可以忽略不计,因为磁盘空间很便宜,而且这类操作通常只是“仅追加”操作,非常之快。 + +当然,在某些边缘情况(以及极少数非常特定的数据库系统)中,起初使用复合主键可能更好。但即便在这些系统里,综合(考虑所有成本)来看,不使用它们并转向其他策略可能更有意义。 + +## 替代方案 + +复合主键的一个替代方案是使用单个自动递增的数值型主键,通常称为 "id",并将原本的复合主键转移为一个由多个字段组成的唯一索引。根据所用主键(取决于预计行数)的不同,"id" 每条记录占用 4 或 8 字节。 + +使用这种策略后,你不再被迫花时间思考并解决上述问题,这将大幅降低不断增长的项目的成本。 + +具体做法是,每个实体都有一个 "id" 字段,通常位于最前面,默认用它来标识唯一行并用于连接。 + +```typescript +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor(public username: string) {} +} +``` + +作为复合主键的替代方案,你可以使用一个多字段唯一索引。 + +```typescript +@entity.index(['tenancyId', 'username'], {unique: true}) +class User { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public tenancyId: number, + public username: string, + ) {} +} +``` + +Deepkit ORM 自动支持递增主键,包括在 MongoDB 中。这是识别数据库记录的首选方式。不过,对于 MongoDB,你也可以使用 ObjectId(`_id: MongoId & PrimaryKey = ''`)作为简单主键。数值型自增主键的替代方案是 UUID,它同样可行(但由于索引代价更高,性能特性会略有不同)。 + +## 总结 + +复合主键本质上意味着:一旦采用,所有后续变更和实践使用都将付出更高成本。虽然起初看起来架构更“干净”(因为少了一列),但一旦项目真正发展起来,就会带来显著的实际成本,而且随着项目变大,这些成本会持续增加。 + +对比利弊的不对称性不难看出,在大多数情况下复合主键并不值得。其成本远大于收益。这不仅对你作为用户如此,对我们作为 ORM 代码的作者与维护者亦然。因此,Deepkit ORM 不支持复合主键。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/entity.md b/website/src/translations/zh/documentation/orm/entity.md new file mode 100644 index 000000000..332e77e9e --- /dev/null +++ b/website/src/translations/zh/documentation/orm/entity.md @@ -0,0 +1,226 @@ +# 实体 + +实体要么是一个类,要么是一个对象字面量(interface),并且始终有一个主键。 +实体使用来自 `@deepkit/type` 的类型注解标注所需的全部信息。例如,定义主键、各种字段及其校验约束。这些字段反映数据库结构,通常对应一张表或一个集合。 + +通过 `Mapped<'name'>` 之类的特殊类型注解,还可以将字段名映射为数据库中的另一个名称。 + +## 类 + +```typescript +import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: string & Unique & MinLength<2> & MaxLength<16>, + public email: string & Unique, + ) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +await database.migrate(); + +await database.persist(new User('Peter')); + +const allUsers = await database.query(User).find(); +console.log('all users', allUsers); +``` + +## 接口 + +```typescript +import { PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + username: string & Unique & MinLength<2> & MaxLength<16>; +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:')); +database.register({name: 'user'}); + +await database.migrate(); + +const user: User = {id: 0, created: new Date, username: 'Peter'}; +await database.persist(user); + +const allUsers = await database.query().find(); +console.log('all users', allUsers); +``` + +## 原始类型 + +诸如 String、Number(bigint)和 Boolean 等原始数据类型会映射到常见的数据库类型。仅使用 TypeScript 的类型即可。 + +```typescript + +interface User { + logins: number; + username: string; + pro: boolean; +} +``` + +## 主键 + +每个实体必须且仅有一个主键。不支持多个主键。 + +主键的基础类型可以是任意类型。常见的是 number 或 UUID。 +对于 MongoDB,常用 MongoId 或 ObjectID。 + +对于数值类型可以使用 `AutoIncrement`。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## 自增 + +需要在插入时自动递增的字段使用 `AutoIncrement` 装饰器标注。所有适配器都支持自增值。MongoDB 适配器会使用一个额外的集合来维护计数器。 + +自增字段是一个自动计数器,只能用于主键。数据库会自动确保 ID 只被使用一次。 + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## UUID + +需要为 UUID(v4)类型的字段使用 UUID 装饰器标注。其运行时类型为 `string`,在数据库中多为二进制存储。使用 `uuid()` 函数可创建一个新的 UUID v4。 + +```typescript +import { uuid, UUID, PrimaryKey } from '@deepkit/type'; + +class User { + id: UUID & PrimaryKey = uuid(); +} +``` + +## MongoDB ObjectID + +在 MongoDB 中应为 ObjectID 类型的字段需使用 `MongoId` 装饰器标注。其运行时类型为 `string`,在数据库中为 `ObjectId`(二进制)。 + +MongoID 字段在插入时会自动获得新值。字段名不必是 `_id`,可以使用任意名称。 + +```typescript +import { PrimaryKey, MongoId } from '@deepkit/type'; + +class User { + id: MongoId & PrimaryKey = ''; +} +``` + +## 可选 / 可空 + +可选字段可通过 TypeScript 的 `title?: string` 或 `title: string | null` 来声明。应只使用其中一种,通常使用可选的 `?` 语法,其在运行时对应 `undefined`。 +对于所有 SQL 适配器,这两种写法都会使数据库中的列类型为 `NULLABLE`。因此它们唯一的区别在于运行时表示的值不同。 + +在下面的示例中,modified 字段是可选的,因此在运行时可以是 undefined,尽管在数据库中始终表示为 NULL。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified?: Date; +} +``` + +此示例展示了可空类型的用法。无论在数据库还是在 JavaScript 运行时都使用 NULL。相较于 `modified?: Date` 更为啰嗦,且不常使用。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +class User { + id: number & PrimaryKey = 0; + modified: Date | null = null; +} +``` + +## 数据库类型映射 + +|=== +|运行时类型|SQLite|MySQL|Postgres|Mongo + +|string|text|longtext|text|string +|number|float|double|double precision|int/number +|boolean|integer(1)|boolean|boolean|boolean +|date|text|datetime|timestamp|datetime +|array|text|json|jsonb|array +|map|text|json|jsonb|object +|map|text|json|jsonb|object +|union|text|json|jsonb|T +|uuid|blob|binary(16)|uuid|binary +|ArrayBuffer/Uint8Array/...|blob|longblob|bytea|binary +|=== + +通过 `DatabaseField` 可以将字段映射为任意数据库类型。该类型必须是有效的 SQL 片段,并会原样传递给迁移系统。 + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + title: string & DatabaseField<{type: 'VARCHAR(244)'}>; +} +``` + +若要针对特定数据库映射字段,可使用 `SQLite`、`MySQL` 或 `Postgres`。 + +### SQLite + +```typescript +import { SQLite } from '@deepkit/type'; + +interface User { + title: string & SQLite<{type: 'text'}>; +} +``` + +### MySQL + +```typescript +import { MySQL } from '@deepkit/type'; + +interface User { + title: string & MySQL<{type: 'text'}>; +} +``` + +### Postgres + +```typescript +import { Postgres } from '@deepkit/type'; + +interface User { + title: string & Postgres<{type: 'text'}>; +} +``` + +## 嵌入类型 + +## 默认值 + +## 默认表达式 + +## 复杂类型 + +## 排除 + +## 数据库特定列类型 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/events.md b/website/src/translations/zh/documentation/orm/events.md new file mode 100644 index 000000000..fc05613ff --- /dev/null +++ b/website/src/translations/zh/documentation/orm/events.md @@ -0,0 +1,63 @@ +# 事件 + +事件是一种接入 Deepkit ORM 的方式,允许你编写强大的插件。事件分为两类:查询事件和工作单元事件。插件作者通常会同时使用这两类事件,以支持这两种操作数据的方式。 + +事件通过 `Database.listen` 在某个事件令牌上注册。也可以在会话上注册短生命周期的事件监听器。 + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); +database.listen(Query.onFetch, async (event) => { +}); + +const session = database.createSession(); + +//只会在此特定会话中执行 +session.eventDispatcher.listen(Query.onFetch, async (event) => { +}); +``` + +## 查询事件 + +当通过 `Database.query()` 或 `Session.query()` 执行查询时会触发查询事件。 + +每个事件都有其附加属性,例如实体类型、查询本身以及数据库会话。你可以通过给 `Event.query` 赋予新的查询来覆盖原始查询。 + +```typescript +import { Query, Database } from '@deepkit/orm'; + +const database = new Database(...); + +const unsubscribe = database.listen(Query.onFetch, async event => { + //覆盖用户的查询,以便执行其他内容。 + event.query = event.query.filterField('fieldName', 123); +}); + +//删除该钩子,调用 unsubscribe +unsubscribe(); +``` + +“Query” 有多个事件令牌: + +| 事件令牌 | 描述 | +|-----------------------|------------------------------------------------------------| +| Query.onFetch | 当通过 find()/findOne()/等 获取对象时 | +| Query.onDeletePre | 在通过 deleteMany/deleteOne() 删除对象之前 | +| Query.onDeletePost | 在通过 deleteMany/deleteOne() 删除对象之后 | +| Query.onPatchPre | 在通过 patchMany/patchOne() 修补/更新对象之前 | +| Query.onPatchPost | 在通过 patchMany/patchOne() 修补/更新对象之后 | + +## 工作单元事件 + +当新会话提交更改时会触发工作单元事件。 + +| 事件令牌 | 描述 | +|-----------------------------|-------------------------------------------------------------------------------------------| +| DatabaseSession.onUpdatePre | 在 `DatabaseSession` 对象对数据库记录发起更新操作之前触发。 | +| DatabaseSession.onUpdatePost | 在 `DatabaseSession` 对象成功完成更新操作之后立即触发。 | +| DatabaseSession.onInsertPre | 在 `DatabaseSession` 对象开始向数据库插入新记录之前触发。 | +| DatabaseSession.onInsertPost | 在 `DatabaseSession` 对象成功插入新记录之后立即触发。 | +| DatabaseSession.onDeletePre | 在 `DatabaseSession` 对象开始执行删除操作以移除数据库记录之前触发。 | +| DatabaseSession.onDeletePost | 在 `DatabaseSession` 对象完成删除操作之后立即触发。 | +| DatabaseSession.onCommitPre | 在 `DatabaseSession` 对象将会话期间的任何更改提交到数据库之前触发。 | \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/getting-started.md b/website/src/translations/zh/documentation/orm/getting-started.md new file mode 100644 index 000000000..3f14412eb --- /dev/null +++ b/website/src/translations/zh/documentation/orm/getting-started.md @@ -0,0 +1,191 @@ +# 快速开始 + +Deepkit 提供了一个数据库 ORM,使得可以以现代的方式访问数据库。 +实体可以用 TypeScript 类型简单地定义: + +```typescript +import { entity, PrimaryKey, AutoIncrement, + Unique, MinLength, MaxLength } from '@deepkit/type'; + +type Username = string & Unique & MinLength<2> & MaxLength<16>; + +// 类实体 +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + firstName?: string; + lastName?: string; + + constructor( + public username: Username, + public email: string & Unique, + ) {} +} + +// 或作为 interface +interface User { + id: number & PrimaryKey & AutoIncrement; + created: Date; + firstName?: string; + lastName?: string; + username: Username; + email: string & Unique; +} +``` + +可以使用任何 TypeScript 类型以及 Deepkit 的验证装饰器来完整定义实体。 +实体类型系统的设计使得这些类型或类也可以用于 HTTP 路由、RPC 操作或前端等其他领域。例如,这可以防止在整个应用中多处重复定义同一个用户。 + +## 安装 + +由于 Deepkit ORM 基于运行时类型,因此必须确保已正确安装 `@deepkit/type`。 +参见[运行时类型安装](../runtime-types/getting-started.md)。 + +如果这一步已成功,则可以安装 `@deepkit/orm` 本体以及一个数据库适配器。 + +如果要使用类作为实体,必须在 tsconfig.json 中启用 `experimentalDecorators`: + +```json +{ + "compilerOptions": { + "experimentalDecorators": true + } +} +``` + +安装库之后,安装一个数据库适配器即可直接使用其 API。 + +### SQLite + +```sh +npm install @deepkit/orm @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; + +const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); +``` + +### MySQL + +```sh +npm install @deepkit/orm @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; + +const database = new Database(new MySQLDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### Postgres + +```sh +npm install @deepkit/orm @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/postgres'; + +const database = new Database(new PostgresDatabaseAdapter({ + host: 'localhost', + port: 3306 +}), [User]); +``` + +### MongoDB + +```sh +npm install @deepkit/orm @deepkit/bson @deepkit/mongo +``` + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(new MongoDatabaseAdapter('mongodb://localhost/mydatabase'), [User]); +``` + +## 用法 + +主要使用 `Database` 对象。实例化后,它可以在整个应用中用于查询或操作数据。到数据库的连接是延迟初始化的。 + +`Database` 对象需要传入一个适配器,该适配器来自相应的数据库适配器库。 + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + @entity.name('user') + class User { + public id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]); + await database.migrate(); // 创建表 + + await database.persist(new User('Peter')); + + const allUsers = await database.query(User).find(); + console.log('all users', allUsers); +} + +main(); +``` + +### 数据库 + +### 连接 + +#### 只读副本 + +## 仓储 + +## 索引 + +## 大小写敏感性 + +## 字符集 + +## 排序规则 + +## 批处理 + +## 缓存 + +## 多租户 + +## 命名策略 + +## 锁定 + +### 乐观锁 + +### 悲观锁 + +## 自定义类型 + +## 日志 + +## 迁移 + +## 数据填充 + +## 原生数据库访问 + +### SQL + +### MongoDB + +## 插件 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/inheritance.md b/website/src/translations/zh/documentation/orm/inheritance.md new file mode 100644 index 000000000..6f40fed57 --- /dev/null +++ b/website/src/translations/zh/documentation/orm/inheritance.md @@ -0,0 +1,104 @@ +# 继承 + +在 Deepkit ORM 中有多种实现继承的方式。 + +## 类继承 + +一种方式是使用类继承,即通过带有 `extends` 的普通类。 + +```typescript +import { PrimaryKey, AutoIncrement } from '@deepkit/type'; + +class BaseModel { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + updated: Date = new Date; +} + +class User extends BaseModel { + name: string = ''; +} + +class Customer extends BaseModel { + name: string = ''; + address: string = ''; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [User, Customer] +); +``` + +由于 `BaseModel` 不会作为实体使用,它不会在数据库中注册。只有 `User` 和 `Customer` 会被注册为实体,并映射到包含 `BaseModel` 所有属性的表。 + +SQL 表如下所示: + +```sql +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL +); + +CREATE TABLE customer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + updated TEXT NOT NULL, + name TEXT NOT NULL, + address TEXT NOT NULL +); +``` + +## 单表继承 + +单表继承是一种将多个实体存储在同一张表中的方式。与为每个模型创建独立的表不同,它使用一张表,并通过一个额外的列(通常命名为 type 或类似名称)来标识每条记录的类型。如果你有许多共享相同属性的实体,这种方式非常有用。 + +```typescript +import { PrimaryKey, AutoIncrement, entity } from '@deepkit/type'; + +@entity.collection('persons') +abstract class Person { + id: number & PrimaryKey & AutoIncrement = 0; + firstName?: string; + lastName?: string; + abstract type: string; +} + +@entity.singleTableInheritance() +class Employee extends Person { + email?: string; + + type: 'employee' = 'employee'; +} + +@entity.singleTableInheritance() +class Freelancer extends Person { + @t budget: number = 10_000; + + type: 'freelancer' = 'freelancer'; +} + +new Database( + new SQLiteDatabaseAdapter('./example.sqlite'), + [Employee, Freelancer] +); +``` + +`Person` 类不是实体,因此不会在数据库中注册。`Employee` 和 `Freelancer` 类是实体,会映射到名为 `persons` 的单个表中。`type` 列用于确定每条记录的类型。 + +SQL 表如下所示: + +```sql +CREATE TABLE persons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + firstName TEXT, + lastName TEXT, + type TEXT NOT NULL, + email TEXT, + budget INTEGER +); +``` + +可以看到,budget 被设为可选(尽管在 `Freelance` 类中它是必需的)。这是为了支持将 `Employee`(没有 budget 值)插入到同一张表中。这是单表继承的一个限制。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/migrations.md b/website/src/translations/zh/documentation/orm/migrations.md new file mode 100644 index 000000000..12de0c9e7 --- /dev/null +++ b/website/src/translations/zh/documentation/orm/migrations.md @@ -0,0 +1,74 @@ +# 迁移 + +迁移是一种以结构化、有组织的方式进行数据库模式变更的方法。它们以 TypeScript 文件的形式存储在某个目录中,并可通过命令行工具执行。 + +在使用 Deepkit Framework 时,Deepkit ORM 的迁移默认启用。 + +## 命令 + +- `migration:create` - 基于数据库差异生成新的迁移文件 +- `migration:pending` - 显示待执行的迁移文件 +- `migration:up` - 执行待执行的迁移文件。 +- `migration:down` - 执行回滚迁移,撤销已执行的旧迁移 + +当你在应用中引入 `FrameworkModule` 时,这些命令可用;或者通过 `@deepkit/sql` 提供的 `deepkit-sql` 命令行工具使用。 + +`FrameworkModule` 的[迁移集成](../framework/database.md#migration)会自动读取你的数据库(你需要将它们定义为 provider),而使用 `deepkit-sql` 时,你需要指定导出数据库的 TypeScript 文件。后者在不使用 Deepkit Framework、单独使用 Deepkit ORM 时很有用。 + +## 创建迁移 + +```typescript +//database.ts +import { Database } from '@deepkit/orm'; +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { User } from './models'; + +export class SQLiteDatabase extends Database { + name = 'default'; + constructor() { + super(new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), [User]); + } +} +``` + +```sh +./node_modules/.bin/deepkit-sql migration:create --path database.ts --migrationDir src/migrations +``` + +一个新的迁移文件会在 `src/migrations` 中创建。 + +新建的迁移文件包含根据 TypeScript 应用中定义的实体与已配置数据库之间的差异生成的 up 和 down 方法。 +你可以按需修改 up 方法。down 方法会基于 up 方法自动生成。 +将该文件提交到你的代码仓库,以便其他开发者也能执行。 + +## 待执行的迁移 + +```sh +./node_modules/.bin/deepkit-sql migration:pending --path database.ts --migrationDir src/migrations +``` + +这将显示所有待执行的迁移。如果你有尚未执行的新迁移文件,它会在这里列出。 + +## 执行迁移 + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations +``` + +这会执行下一个待执行的迁移。 + +## 回滚迁移 + +```sh +./node_modules/.bin/deepkit-sql migration:down --path database.ts --migrationDir src/migrations +``` + +这会回滚上一次执行的迁移。 + +## 伪迁移 + +假设你想执行一次迁移(up 或 down),但执行失败。你已手动修复了问题,但现在无法再次执行该迁移,因为它已被标记为已执行。你可以使用 `--fake` 选项来“伪执行”该迁移,使其在数据库中被标记为已执行而不实际执行。这样你就可以继续执行下一个待执行的迁移。 + +```sh +./node_modules/.bin/deepkit-sql migration:up --path database.ts --migrationDir src/migrations --fake +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/orm-browser.md b/website/src/translations/zh/documentation/orm/orm-browser.md new file mode 100644 index 000000000..5e1dda836 --- /dev/null +++ b/website/src/translations/zh/documentation/orm/orm-browser.md @@ -0,0 +1,60 @@ +# ORM 浏览器 + +Deepkit ORM 浏览器是一个基于 Web 的工具,用于探索你的数据库架构和数据。它构建在 Deepkit 框架之上,可用于 Deepkit ORM 支持的任何数据库。 + +![ORM 浏览器](/assets/screenshots-orm-browser/content-editing.png) + +## 安装 + +Deepkit ORM 浏览器是 Deepkit 框架的一部分,并且在启用调试模式时会自动启用。 + +```typescript +import { App } from '@deepkit/app'; +import { Database } from '@deepkit/orm'; + +class MyController { + @http.GET('/') + index() { + return 'Hello World'; + } +} + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +new App({ + controllers: [MyController], + providers: [MainDatabase], + imports: [new FrameworkModule({debug: true})], +}).run(); +``` + +或者,你可以将 Deepkit ORM 浏览器作为独立包安装。 + +```bash +npm install @deepkit/orm-browser +``` + +```typescript +// database.ts +import { Database } from '@deepkit/orm'; + +class MainDatabase extends Database { + constructor() { + super(new DatabaseAdapterSQLite()); + } +} + +export const database = new MainDatabase(); +``` + +接下来,可以启动 Deepkit ORM 浏览器服务器。 + +```sh +./node_modules/.bin/deepkit-orm-browser database.ts +``` + +现在可以通过 http://localhost:9090 访问 Deepkit ORM 浏览器。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/plugin-soft-delete.md b/website/src/translations/zh/documentation/orm/plugin-soft-delete.md new file mode 100644 index 000000000..48525343d --- /dev/null +++ b/website/src/translations/zh/documentation/orm/plugin-soft-delete.md @@ -0,0 +1,112 @@ +# 软删除 + +软删除插件允许在不实际删除数据库记录的情况下将其隐藏。当一条记录被删除时,它只会被标记为已删除,而不会真正删除。所有查询会自动根据这个已删除属性进行过滤,因此对用户而言就像真的被删除了一样。 + +要使用该插件,必须实例化 SoftDelete 类并为每个实体启用它。 + +```typescript +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { SoftDelete } from '@deepkit/orm'; + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + // 此字段用于标识该记录是否被软删除。 + // 如果已设置,则表示该记录被软删除。 + deletedAt?: Date; + + // 此字段是可选的,可用于追踪是谁/什么删除了该记录。 + deletedBy?: string; + + constructor( + public name: string + ) { + } +} + +const softDelete = new SoftDelete(database); +softDelete.enable(User); + +//或者再次禁用 +softDelete.disable(User); +``` + +## 删除 + +要进行软删除,请使用常规方法:在查询中使用 `deleteOne` 或 `deleteMany`,或使用会话删除它们。软删除插件将自动在后台完成其余工作。 + +## 恢复 + +已删除的记录可以通过通过 `SoftDeleteQuery` 的“提升”查询进行恢复。它提供 `restoreOne` 和 `restoreMany`。 + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreOne(); +await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreMany(); +``` + +会话也支持恢复实体。 + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).restore(user1); +await session.commit(); +``` + +## 硬删除 + +要进行硬删除,请通过 SoftDeleteQuery 使用提升查询。这实质上恢复到了未使用软删除插件时的正常行为。 + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// 真正从数据库中删除该记录 +await database.query(User).lift(SoftDeleteQuery).hardDeleteOne(); +await database.query(User).lift(SoftDeleteQuery).hardDeleteMany(); + +// 这些与上面的等价 +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteOne(); +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteMany(); +``` + +## 查询已删除。 + +通过 `SoftDeleteQuery` 的“提升”查询,你也可以包含已删除的记录。 + +```typescript +import { SoftDeleteQuery } from '@deepkit/orm'; + +// 查找所有,包括软删除和未删除的 +await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().find(); + +// 仅查找软删除的 +await database.query(s).lift(SoftDeleteQuery).isSoftDeleted().count() +``` + +## 删除者 + +可以通过查询和会话设置 `deletedBy`。 + +```typescript +import { SoftDeleteSession } from '@deepkit/orm'; + +const session = database.createSession(); +const user1 = session.query(User).findOne(); + +session.from(SoftDeleteSession).setDeletedBy('Peter'); +session.remove(user1); + +await session.commit(); +import { SoftDeleteQuery } from '@deepkit/orm'; + +database.query(User).lift(SoftDeleteQuery) +.deletedBy('Peter') +.deleteMany(); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/query.md b/website/src/translations/zh/documentation/orm/query.md new file mode 100644 index 000000000..37a427304 --- /dev/null +++ b/website/src/translations/zh/documentation/orm/query.md @@ -0,0 +1,357 @@ +# 查询 + +查询是一个对象,用于描述如何从数据库检索或修改数据。它有多种方法来描述查询,以及执行它们的终止方法。数据库适配器可以通过多种方式扩展查询 API,以支持数据库特定的功能。 + +你可以使用 `Database.query(T)` 或 `Session.query(T)` 创建查询。我们建议使用 Session,因为它能提升性能。 + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + birthdate?: Date; + visits: number = 0; + + constructor(public username: string) { + } +} + +const database = new Database(...); + +//[ { username: 'User1' }, { username: 'User2' }, { username: 'User2' } ] +const users = await database.query(User).select('username').find(); +``` + +## 过滤器 + +可以应用过滤器来限制结果集。 + +```typescript +// 简单过滤器 +const users = await database.query(User).filter({name: 'User1'}).find(); + +// 多个过滤器,全部 AND +const users = await database.query(User).filter({name: 'User1', id: 2}).find(); + +// 范围过滤:$gt, $lt, $gte, $lte(大于,小于,...) +// 等价于 WHERE created < NOW() +const users = await database.query(User).filter({created: {$lt: new Date}}).find(); +// 等价于 WHERE id > 500 +const users = await database.query(User).filter({id: {$gt: 500}}).find(); +// 等价于 WHERE id >= 500 +const users = await database.query(User).filter({id: {$gte: 500}}).find(); + +// 集合过滤:$in, $nin(在,不在) +// 等价于 WHERE id IN (1, 2, 3) +const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find(); + +// 正则过滤 +const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find(); + +// 分组:$and, $nor, $or +// 等价于 WHERE (username = 'User1') OR (username = 'User2') +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2'}] +}).find(); + + +// 嵌套分组 +// 等价于 WHERE username = 'User1' OR (username = 'User2' and id > 0) +const users = await database.query(User).filter({ + $or: [{username: 'User1'}, {username: 'User2', id: {$gt: 0}}] +}).find(); + + +// 嵌套分组 +// 等价于 WHERE username = 'User1' AND (created < NOW() OR id > 0) +const users = await database.query(User).filter({ + $and: [{username: 'User1'}, {$or: [{created: {$lt: new Date}, id: {$gt: 0}}]}] +}).find(); +``` + +### 等于 + +### 大于 / 小于 + +### 正则表达式 + +### 分组 AND/OR + +### In + +## 选择字段 + +要缩小从数据库接收的字段范围,可以使用 `select('field1')`。 + +```typescript +const user = await database.query(User).select('username').findOne(); +const user = await database.query(User).select('id', 'username').findOne(); +``` + +需要注意的是,一旦使用 `select` 缩小了字段范围,结果将不再是实体的实例,而只是普通对象字面量。 + +``` +const user = await database.query(User).select('username').findOne(); +user instanceof User; //false +``` + +## 排序 + +使用 `orderBy(field, order)` 可以改变条目的顺序。 +可以多次调用 `orderBy` 以进一步细化排序。 + +```typescript +const users = await session.query(User).orderBy('created', 'desc').find(); +const users = await session.query(User).orderBy('created', 'asc').find(); +``` + +## 分页 + +可以使用 `itemsPerPage()` 和 `page()` 方法对结果进行分页。页码从 1 开始。 + +```typescript +const users = await session.query(User).itemsPerPage(50).page(1).find(); +``` + +通过备用方法 `limit` 和 `skip` 也可以手动分页。 + +```typescript +const users = await session.query(User).limit(5).skip(10).find(); +``` + +[#database-join] +## 连接(Join) + +默认情况下,实体中的引用既不会包含在查询中也不会被加载。若要在查询中包含连接但不加载引用,请使用 `join()`(左连接)或 `innerJoin()`。若要在查询中包含连接并加载引用,请使用 `joinWith()` 或 `innerJoinWith()`。 + +以下所有示例都假设这些模型模式: + +```typescript +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + group?: Group & Reference; + + constructor(public username: string) { + } +} +``` + +```typescript +// 只选择有分配到组的用户(INNER JOIN) +const users = await session.query(User).innerJoin('group').find(); +for (const user of users) { + user.group; // 报错,因为引用未被加载 +} +``` + +```typescript +// 只选择有分配到组的用户(INNER JOIN)并加载关系 +const users = await session.query(User).innerJoinWith('group').find(); +for (const user of users) { + user.group.name; // 可用 +} +``` + +要修改连接查询,请使用相同的方法,但加上 `use` 前缀:`useJoin`、`useInnerJoin`、`useJoinWith` 或 `useInnerJoinWith`。要结束连接查询的修改,使用 `end()` 回到父查询。 + +```typescript +// 只选择分配了名为 'admins' 的组的用户(INNER JOIN) +const users = await session.query(User) + .useInnerJoinWith('group') + .filter({name: 'admins'}) + .end() // 返回到父查询 + .find(); + +for (const user of users) { + user.group.name; // 始终为 admin +} +``` + +## 聚合 + +聚合方法允许你统计记录并聚合字段。 + +以下示例假设此模型模式: + +```typescript +@entity.name('file') +class File { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + downloads: number = 0; + + category: string = 'none'; + + constructor(public path: string & Index) { + } +} +``` + +`groupBy` 允许按指定字段对结果分组。 + +```typescript +await database.persist( + cast({path: 'file1', category: 'images'}), + cast({path: 'file2', category: 'images'}), + cast({path: 'file3', category: 'pdfs'}) +); + +//[ { category: 'images' }, { category: 'pdfs' } ] +await session.query(File).groupBy('category').find(); +``` + +有多种聚合方法:`withSum`、`withAverage`、`withCount`、`withMin`、`withMax`、`withGroupConcat`。每个方法都需要字段名作为第一个参数,并可选地提供第二个参数以更改别名。 + +```typescript +// 首先我们更新部分记录: +await database.query(File).filter({path: 'images/file1'}).patchOne({$inc: {downloads: 15}}); +await database.query(File).filter({path: 'images/file2'}).patchOne({$inc: {downloads: 5}}); + +//[{ category: 'images', downloads: 20 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withSum('downloads').find(); + +//[{ category: 'images', downloads: 10 },{ category: 'pdfs', downloads: 0 }] +await session.query(File).groupBy('category').withAverage('downloads').find(); + +//[ { category: 'images', amount: 2 }, { category: 'pdfs', amount: 1 } ] +await session.query(File).groupBy('category').withCount('id', 'amount').find(); +``` + +## 返回(Returning) + +使用 `returning` 可以在通过 `patch` 和 `delete` 更改数据时额外请求字段。 + +注意:并非所有数据库适配器都会以原子方式返回字段。请使用事务以确保数据一致性。 + +```typescript +await database.query(User).patchMany({visits: 0}); + +//{ modified: 1, returning: { visits: [ 5 ] }, primaryKeys: [ 1 ] } +const result = await database.query(User) + .filter({username: 'User1'}) + .returning('username', 'visits') + .patchOne({$inc: {visits: 5}}); +``` + +## Find + +返回匹配指定过滤器的条目数组。 + +```typescript +const users: User[] = await database.query(User).filter({username: 'Peter'}).find(); +``` + +## FindOne + +返回匹配指定过滤器的条目。 +如果未找到条目,将抛出 `ItemNotFound` 错误。 + +```typescript +const users: User = await database.query(User).filter({username: 'Peter'}).findOne(); +``` + +## FindOneOrUndefined + +返回匹配指定过滤器的条目。 +如果未找到条目,则返回 undefined。 + +```typescript +const query = database.query(User).filter({username: 'Peter'}); +const users: User|undefined = await query.findOneOrUndefined(); +``` + +## FindField + +返回匹配指定过滤器的某个字段的列表。 + +```typescript +const usernames: string[] = await database.query(User).findField('username'); +``` + +## FindOneField + +返回匹配指定过滤器的某个字段的值。 +如果未找到条目,将抛出 `ItemNotFound` 错误。 + +```typescript +const username: string = await database.query(User).filter({id: 3}).findOneField('username'); +``` + +## Patch + +Patch 是一种更改查询,用于修改查询所描述的记录。方法 +`patchOne` 和 `patchMany` 会结束查询并执行补丁操作。 + +`patchMany` 会更改数据库中符合指定过滤器的所有记录。如果未设置过滤器,整个表都会被更改。使用 `patchOne` 可一次仅更改一个条目。 + +```typescript +await database.query(User).filter({username: 'Peter'}).patch({username: 'Peter2'}); + +await database.query(User).filter({username: 'User1'}).patchOne({birthdate: new Date}); +await database.query(User).filter({username: 'User1'}).patchOne({$inc: {visits: 1}}); + +await database.query(User).patchMany({visits: 0}); +``` + +## 删除 + +`deleteMany` 会删除数据库中所有匹配指定过滤器的条目。 +如果未设置过滤器,整个表都会被删除。使用 `deleteOne` 可一次仅删除一个条目。 + +```typescript +const result = await database.query(User) + .filter({visits: 0}) + .deleteMany(); + +const result = await database.query(User).filter({id: 4}).deleteOne(); +``` + +## Has + +返回数据库中是否至少存在一个条目。 + +```typescript +const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has(); +``` + +## 计数 + +返回条目数量。 + +```typescript +const userCount: number = await database.query(User).count(); +``` + +## 提升(Lift) + +对查询进行“提升”意味着向其添加新功能。这通常由插件或复杂架构使用,以将较大的查询类拆分为多个方便、可复用的类。 + +```typescript +import { FilterQuery, Query } from '@deepkit/orm'; + +class UserQuery extends Query { + hasBirthday() { + const start = new Date(); + start.setHours(0,0,0,0); + const end = new Date(); + end.setHours(23,59,59,999); + + return this.filter({$and: [{birthdate: {$gte: start}}, {birthdate: {$lte: end}}]} as FilterQuery); + } +} + +await session.query(User).lift(UserQuery).hasBirthday().find(); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/raw-access.md b/website/src/translations/zh/documentation/orm/raw-access.md new file mode 100644 index 000000000..5da0ede27 --- /dev/null +++ b/website/src/translations/zh/documentation/orm/raw-access.md @@ -0,0 +1,77 @@ +# 原始访问 + +经常需要直接访问数据库,例如运行 ORM 不支持的 SQL 查询。这可以通过 `Database` 类上的 `raw` 方法完成。 + +```typescript +import { PrimaryKey, AutoIncrement, @entity } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; +import { sql } from '@deepkit/sql'; +import { SqliteDatabaseAdapter } from '@deepkit/sqlite'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + constructor(public username: string) {} +} + +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + +const query = 'Pet%'; +const rows = await database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`).find(); + +const result = await database.raw<{ count: number }>(sql`SELECT count(*) as count FROM users WHERE username LIKE ${query}`).findOne(); +console.log('Found', result.count, 'users'); +``` + +SQL 查询是使用 `sql` 模板字符串标签构建的。这是一个特殊的模板字符串标签,允许以参数的形式传递值。这些参数随后会被自动解析并转换为安全的预处理语句。这对于避免 SQL 注入攻击非常重要。 + +要传递列名等动态标识符,可以使用 `identifier`: + +```typescript +import { identifier, sql } from '@deepkit/sql'; + +let column = 'username'; +const rows = await database.raw(sql`SELECT * FROM users WHERE ${identifier(column)} LIKE ${query}`).find(); +``` + +对于 SQL 适配器,`raw` 方法会返回一个带有 `findOne` 和 `find` 方法以获取结果的 `RawQuery`。要执行不返回行(如 UPDATE/DELETE/等)的 SQL,可使用 `execute`: + +```typescript +let username = 'Peter'; +await database.raw(sql`UPDATE users SET username = ${username} WHERE id = 1`).execute(); +``` + +`RawQuery` 还支持获取最终的 SQL 字符串和参数,并为数据库适配器正确格式化: + +```typescript +const query = database.raw(sql`SELECT * FROM users WHERE username LIKE ${query}`); +console.log(query.sql); +console.log(query.params); +``` + +这样,SQL 也可以在其他数据库客户端中执行,例如。 + +## 类型 + +请注意,你可以向 `raw` 传递任意类型,数据库返回的结果会被自动转换为该类型。这对 SQL 适配器尤其有用,你可以传递一个类,返回结果会自动转换为该类的实例。 + +但这也有局限性。以这种方式不支持 SQL JOIN。如果你想使用 JOIN,必须使用 ORM 的查询构建器。 + +## Mongo + +MongoDB 适配器的工作方式稍有不同,因为它不是基于 SQL 查询,而是基于 Mongo 命令。 + +命令可以是聚合管道、查找查询或写入命令。 + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; + +const database = new Database(MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase')); + +// 第一个参数是入口集合,第二个是命令的返回类型 +const items = await database.raw([ + { $match: { roomId: 'room1' } }, + { $group: { _id: '$userId', count: { $sum: 1 } } }, +]).find(); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/relations.md b/website/src/translations/zh/documentation/orm/relations.md new file mode 100644 index 000000000..1577abb15 --- /dev/null +++ b/website/src/translations/zh/documentation/orm/relations.md @@ -0,0 +1,153 @@ +# 关系 + +关系允许以某种方式将两个实体连接起来。这通常在数据库中通过外键的概念来实现。Deepkit ORM 在所有官方数据库适配器中都支持关系。 + +关系使用 `Reference` 装饰器进行标注。通常一个关系还会有一个反向关系,用 `BackReference` 类型标注,但只有在需要在数据库查询中使用反向关系时才需要。反向引用仅为虚拟的。 + +## 一对多 + +存储引用的实体通常称为 `owning side`,即 `owns` 该引用的一方。下面的代码展示了 `User` 与 `Post` 之间的一对多关系的两个实体。这意味着一个 `User` 可以拥有多个 `Post`。实体 `post` 拥有 `post->user` 关联。在数据库中,此时会有一个字段 `Post. "author"`,其中包含 `User` 的主键。 + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement, + Reference } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +@entity.collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public username: string) { + } +} + +@entity.collection('posts') +class Post { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor( + public author: User & Reference, + public title: string + ) { + } +} + +const database = new Database( + new SQLiteDatabaseAdapter(':memory:'), + [User, Post] +); +await database.migrate(); + +const user1 = new User('User1'); +const post1 = new Post(user1, 'My first blog post'); +const post2 = new Post(user1, 'My second blog post'); + +await database.persist(user1, post1, post2); +``` + +默认情况下,查询不会选取引用。详见[数据库联接](./query.md#join)。 + +## 多对一 + +一个引用通常会有一个反向引用(多对一)。它只是一个虚拟引用,因为它并不会在数据库中体现。反向引用使用 `BackReference` 标注,主要用于反射与查询联接。如果在 `User` 上添加到 `Post` 的 `BackReference`,就可以在 `User` 的查询中直接联接 `Post`。 + +```typescript +@entity.name('user').collection('users') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + posts?: Post[] & BackReference; + + constructor(public username: string) { + } +} +``` + +```typescript +//[ { username: 'User1', posts: [ [Post], [Post] ] } ] +const users = await database.query(User) + .select('username', 'posts') + .joinWith('posts') + .find(); +``` + +## 多对多 + +多对多关系允许将多个记录与多个其他记录关联起来。例如,它可用于用户与组的场景。一个用户可以不在任何组、在一个组或多个组中。相应地,一个组可以包含 0 个、一个或多个用户。 + +多对多关系通常使用一个中间表实体来实现。该中间实体包含指向另外两个实体的实际拥有端引用,而这两个实体则对该中间实体设置反向引用。 + +```typescript +@entity.name('user') +class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + groups?: Group[] & BackReference<{via: typeof UserGroup}>; + + constructor(public username: string) { + } +} + +@entity.name('group') +class Group { + id: number & PrimaryKey & AutoIncrement = 0; + + users?: User[] & BackReference<{via: typeof UserGroup}>; + + constructor(public name: string) { + } +} + +// 中间表实体 +@entity.name('userGroup') +class UserGroup { + id: number & PrimaryKey & AutoIncrement = 0; + + constructor( + public user: User & Reference, + public group: Group & Reference, + ) { + } +} +``` + +使用这些实体后,你可以创建用户与组,并通过中间实体将它们关联起来。通过在 User 中使用反向引用,我们可以在 User 的查询中直接获取其 groups。 + +```typescript +const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Group, UserGroup]); +await database.migrate(); + +const user1 = new User('User1'); +const user2 = new User('User2'); +const group1 = new Group('Group1'); + +await database.persist(user1, user2, group1, new UserGroup(user1, group1), new UserGroup(user2, group1)); + +//[ +// { id: 1, username: 'User1', groups: [ [Group] ] }, +// { id: 2, username: 'User2', groups: [ [Group] ] } +// ] +const users = await database.query(User) + .select('username', 'groups') + .joinWith('groups') + .find(); +``` + +要将用户与组解除关联,删除对应的 UserGroup 记录即可: + +```typescript +const users = await database.query(UserGroup) + .filter({user: user1, group: group1}) + .deleteOne(); +``` + +## 一对一 + +## 约束 + +删除/更新时:RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/seeding.md b/website/src/translations/zh/documentation/orm/seeding.md new file mode 100644 index 000000000..093fa749c --- /dev/null +++ b/website/src/translations/zh/documentation/orm/seeding.md @@ -0,0 +1,3 @@ +# 数据填充 + +数据库填充是向数据库写入初始数据的过程。它是一种数据库初始数据的预置方式,旨在用于开发、测试或生产环境的数据库初始化。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/session.md b/website/src/translations/zh/documentation/orm/session.md new file mode 100644 index 000000000..f14ae67f9 --- /dev/null +++ b/website/src/translations/zh/documentation/orm/session.md @@ -0,0 +1,59 @@ +# 会话 / 工作单元 + +会话类似于一个工作单元。它会跟踪你所做的一切,并在调用 `commit()` 时自动记录更改。它是执行数据库更改的首选方式,因为它以一种非常快速的方式将语句打包。会话非常轻量,例如,可以在请求-响应生命周期中轻松创建。 + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type'; +import { Database } from '@deepkit/orm'; + +async function main() { + + @entity.name('user') + class User { + id: number & PrimaryKey & AutoIncrement = 0; + created: Date = new Date; + + constructor(public name: string) { + } + } + + const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]); + await database.migrate(); + + const session = database.createSession(); + session.add(new User('User1'), new User('User2'), new User('User3')); + + await session.commit(); + + const users = await session.query(User).find(); + console.log(users); +} + +main(); +``` + +使用 `session.add(T)` 将新实例添加到会话,或使用 `session.remove(T)` 移除现有实例。完成对 Session 对象的使用后,只需在所有地方取消对它的引用,以便垃圾回收器可以将其回收。 + +通过 Session 对象获取的实体实例,其更改会被自动检测。 + +```typescript +const users = await session.query(User).find(); +for (const user of users) { + user.name += ' changed'; +} + +await session.commit();//保存所有用户 +``` + +## 标识映射 + +会话提供标识映射,确保每个数据库记录只对应一个 JavaScript 对象。比如,如果你在同一个会话中两次运行 `session.query(User).find()`,你会得到两个不同的数组,但其中包含相同的实体实例。 + +如果你使用 `session.add(entity1)` 添加了一个新实体,然后再次获取它,你会得到完全相同的实体实例 `entity1`。 + +重要提示:一旦开始使用会话,你应使用其 `session.query` 方法而不是 `database.query`。只有会话查询启用了标识映射功能。 + +## 变更检测 + +## 请求/响应 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/orm/transactions.md b/website/src/translations/zh/documentation/orm/transactions.md new file mode 100644 index 000000000..3b32c14ac --- /dev/null +++ b/website/src/translations/zh/documentation/orm/transactions.md @@ -0,0 +1,85 @@ +# 事务 + +事务是按顺序执行的一组语句、查询或操作(如 select、insert、update 或 delete),它们作为单个工作单元执行,可以提交或回滚。 + +Deepkit 为所有官方支持的数据库提供事务支持。默认情况下,任何查询或数据库会话都不使用事务。要启用事务,有两种主要方法:会话和回调。 + +## 会话事务 + +你可以为每个创建的会话启动并分配一个新事务。这是与数据库交互的首选方式,因为你可以轻松传递 Session 对象,并且由该会话实例化的所有查询都会自动归入其事务。 + +一种典型的模式是将所有操作包裹在 try-catch 块中,并在最后一行执行 `commit()`(仅当之前的所有命令都成功时才会执行),在 catch 块中执行 `rollback()`,以便一旦发生错误就回滚所有更改。 + +尽管存在替代的 API(见下文),但所有事务仅适用于数据库会话对象。要将数据库会话中工作单元的未提交更改持久化到数据库,通常调用 `commit()`。在事务性会话中,`commit()` 不仅会将所有待处理更改提交到数据库,还会完成(“提交”)事务,从而关闭该事务。或者,你可以调用 `session.flush()` 在不调用 `commit` 的情况下提交所有待处理更改,从而不会关闭事务。若要在不刷新工作单元的情况下提交事务,请使用 `session.commitTransaction()`。 + +```typescript +const session = database.createSession(); + +// 这会分配一个新事务,并在下一次数据库操作时启动它。 +session.useTransaction(); + +try { + // 该查询在事务中执行 + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); + + await session.commit(); +} catch (error) { + await session.rollback(); +} +``` + +一旦 `commit()` 或 `rollback()` 在会话中执行,该事务就会被释放。如果你想在新事务中继续,就必须再次调用 `useTransaction()`。 + +请注意,一旦在事务性会话中执行了第一条数据库操作,分配的数据库连接将固定并独占于当前会话对象(粘性)。因此,所有后续操作都会在同一连接上执行(从而在大多数数据库中,也是在同一数据库服务器上)。只有当事务性会话终止(commit 或 rollback)时,数据库连接才会被再次释放。因此,建议尽可能缩短事务的持续时间。 + +如果一个会话已经连接到某个事务,调用 `session.useTransaction()` 将始终返回同一对象。使用 `session.isTransaction()` 来检查会话是否关联了事务。 + +不支持嵌套事务。 + +## 事务回调 + +事务性会话的另一种替代方式是 `database.transaction(callback)`。 + +```typescript +await database.transaction(async (session) => { + // 该查询在事务中执行 + const users = await session.query(User).find(); + + await moreDatabaseOperations(session); +}); +``` + +`database.transaction(callback)` 方法会在一个新的事务性会话中执行异步回调。若回调成功(即未抛出错误),该会话会自动提交(其事务被提交,且所有更改被刷新)。若回调失败,会话会自动执行 `rollback()`,并将错误向外传播。 + +## 隔离级别 + +许多数据库支持不同类型的事务。要更改事务行为,你可以对 `useTransaction()` 返回的事务对象调用不同的方法。该事务对象的接口取决于所使用的数据库适配器。例如,来自 MySQL 数据库的事务对象与来自 MongoDB 的事务对象具有不同的选项。使用代码补全或查看数据库适配器的接口以获取可用选项列表。 + +```typescript +const database = new Database(new MySQLDatabaseAdapter()); + +const session = database.createSession(); +session.useTransaction().readUncommitted(); + +try { + //...操作 + await session.commit(); +} catch (error) { + await session.rollback(); +} + +// 或者 +await database.transaction(async (session) => { + // 只要尚未执行任何数据库操作,这是可行的。 + session.useTransaction().readUncommitted(); + + //...操作 +}); +``` + +虽然 MySQL、PostgreSQL 和 SQLite 的事务默认即可使用,但对 MongoDB,你必须先将其设置为“副本集”。 + +要将标准的 MongoDB 实例转换为副本集,请参阅官方文档链接: +[将独立部署转换为副本集](https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set). \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/angular-ssr.md b/website/src/translations/zh/documentation/package/angular-ssr.md new file mode 100644 index 000000000..528446aef --- /dev/null +++ b/website/src/translations/zh/documentation/package/angular-ssr.md @@ -0,0 +1,128 @@ +# API `@deepkit/angular-ssr` + +```shell +npm install @deepkit/angular-ssr +``` + + +- 请确保将你的主应用程序放在 `app.ts` 中,并在 `angular.json` 中进行配置: + +在 `src/server/app.ts`: + +```typescript +import { App } from '@deepkit/app'; +import { FrameworkModule } from '@deepkit/framework'; +import { AngularModule, RequestHandler } from '@deepkit/angular-ssr'; + +const app = new App({ + controllers: [ + // 你的控制器 + ], + providers: [ + // 你的提供者 + ], + imports: [ + new FrameworkModule({}), + new AngularModule({ + moduleUrl: import.meta.url, + }) + ] +}); + +const main = isMainModule(import.meta.url); + +if (main) { + void app.run(); // 允许调用所有 CLI 命令,包括 server:start +} + +export const reqHandler = main + // 处于 main 时,我们不希望创建新的请求处理器 + ? () => undefined + : app.get(RequestHandler).create(); +``` + +```json +{ + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/app", + "index": "src/index.html", + "server": "src/main.server.ts", + "outputMode": "server", + "ssr": { + "entry": "src/server/app.ts" + }, + "browser": "src/main.ts" + } +} +``` + +请确保在 tsconfig 中也包含 `src/server/app.ts`。 + +## 配置 Angular 应用 + +在 `app/app.config.ts`(客户端): + +```typescript +@Injectable() +export class APIInterceptor implements HttpInterceptor { + constructor(@Inject('baseUrl') @Optional() private baseUrl: string) { + // 在客户端构建中,`baseUrl` 为空,应从当前地址推断。 + // 如果这不符合你的需求,你可以直接在 `appConfig` 对象的 `providers` 数组中定义 `baseUrl`。 + this.baseUrl = baseUrl || (typeof location !== 'undefined' ? location.origin : ''); + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const apiReq = req.clone({ url: `${this.baseUrl}/${req.url}` }); + return next.handle(apiReq); + } +} + +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: APIInterceptor, + multi: true, + }, + // 你的其他提供者 + ], +}; +``` + +在 `app/app.server.config.ts`(服务端): + +```typescript +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering(), + provideServerRouting([ + { + path: '**', + renderMode: RenderMode.Server, + }, + ]), + { + provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP, + deps: [REQUEST_CONTEXT], + useFactory(context: any) { + return { [context?.baseUrl]: context?.publicBaseUrl || '' }; + }, + }, + { + provide: 'baseUrl', + deps: [REQUEST_CONTEXT], + useFactory: (context: any) => { + return context?.baseUrl || ''; + }, + } + ], +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); +``` + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/api-console.md b/website/src/translations/zh/documentation/package/api-console.md new file mode 100644 index 000000000..edd12592e --- /dev/null +++ b/website/src/translations/zh/documentation/package/api-console.md @@ -0,0 +1,40 @@ +# Deepkit API 控制台 + +```bash +npms install @deepkit/api-console-module +``` + +HTTP 和 RPC API 的自动文档,以 TypeScript 类型语法展示所有路由、操作、参数、返回类型、状态码。 + +它是 [框架调试器](../framework.md) 的一部分,但也可以单独使用。 + +```typescript +import { ApiConsoleModule } from '@deepkit/api-console-module'; + +new App({ + imports: [ + new ApiConsoleModule({ + path: '/api', + markdown: ` + # My API + + This is my API documentation. + + Have fun! + ` + }), + ] +}) +``` + +默认情况下,`new ApiConsoleModule` 会显示所有 HTTP 和 RPC 路由。你也可以通过 `ApiConsoleModule` 类上的方法指定要显示的路由。 + + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/app.md b/website/src/translations/zh/documentation/package/app.md new file mode 100644 index 000000000..38fa7c8a2 --- /dev/null +++ b/website/src/translations/zh/documentation/package/app.md @@ -0,0 +1,14 @@ +# API `@deepkit/app` + +```shell +npm install @deepkit/app +``` + +提供命令行界面解析器、服务容器与依赖注入、事件分发器、应用模块系统,以及配置加载器。 + +这是编写 Deepkit 应用的核心。 +你可以注册 CLI 控制器、HTTP 控制器或路由、RPC 控制器(通过[框架模块](../framework.md)),以及其他服务。 + +更多信息请参阅[Deepkit 应用文档](../app.md)。 + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/bench.md b/website/src/translations/zh/documentation/package/bench.md new file mode 100644 index 000000000..727047fc1 --- /dev/null +++ b/website/src/translations/zh/documentation/package/bench.md @@ -0,0 +1,35 @@ +# API `@deepkit/bench` + +```sh +npm install @deepkit/bench +``` + +用于基准测试代码片段的简单工具。 + +```typescript +import { benchmark, run } from '@deepkit/bench'; + +// ASCII 二进制解析示例 +const binaryString = Buffer.from('Hello World', 'utf8'); +const codes = [ + +benchmark('Buffer.toString', () => { + const utf8String = binaryString.toString('utf8'); +}); + +benchmark('String.fromCodePoint', () => { + const utf8String = String.fromCodePoint() +}); + +void run(); +``` + +```sh +$ node --import @deepkit/run benchmarks/ascii-parsing.ts +Node v22.13.1 + 🏎 x 20,326,482.53 ops/sec ± 4.95% 0.000049 ms/op ▆▆▇▅▆▆▅▅▆▅▅▅▅▅▅▅▅▅▅▅▅▅ Buffer.toString 19850001 samples + 🏎 x 36,012,545.69 ops/sec ± 1.78% 0.000028 ms/op ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ String.fromCodePoint 35800001 samples +done +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/broker-redis.md b/website/src/translations/zh/documentation/package/broker-redis.md new file mode 100644 index 000000000..18960fbd1 --- /dev/null +++ b/website/src/translations/zh/documentation/package/broker-redis.md @@ -0,0 +1,29 @@ +# API `@deepkit/broker-redis` + +```sh +npm install @deepkit/broker-redis +``` + +提供 Deepkit Broker 的基于 Redis 的实现。其底层使用 ioredis。 + +此适配器不实现 Deepkit Broker 的队列适配器功能。 + +```typescript +import { BrokerKeyValue, BrokerBus } from '@deepkit/broker'; +import { BrokerRedisAdapter } from '@deepkit/broker-redis'; +import { ConsoleLogger } from '@deepkit/logger'; + +const adapter = new RedisBrokerAdapter({ + preifx: 'myapp:', + host: 'localhost', + port: 6379, + // password: 'your-password', // 可选项,如果你的 Redis 服务器需要身份验证 + // db: 0, // 可选项,用于指定不同的 Redis 数据库 +}, new ConsoleLogger()); + +const keyValye = new BrokerKeyValue(adapter); +const bus = new BrokerBus(adapter); +// ... +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/broker.md b/website/src/translations/zh/documentation/package/broker.md new file mode 100644 index 000000000..4f0007059 --- /dev/null +++ b/website/src/translations/zh/documentation/package/broker.md @@ -0,0 +1,9 @@ +# API `@deepkit/broker` + +```sh +npm install @deepkit/broker +``` + +该包包含 Deepkit Broker 的抽象、服务器实现,以及作为适配器的多个客户端实现(`BrokerDeepkitAdapter`、`BrokerMemoryAdapter`)。 + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/bson.md b/website/src/translations/zh/documentation/package/bson.md new file mode 100644 index 000000000..3431a2014 --- /dev/null +++ b/website/src/translations/zh/documentation/package/bson.md @@ -0,0 +1,9 @@ +# API `@deepkit/bson` + +```shell +npm install @deepkit/bson +``` + +提供用于在 BSON 与 JavaScript 对象之间转换的编码器/解码器函数。 + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/bun.md b/website/src/translations/zh/documentation/package/bun.md new file mode 100644 index 000000000..ebc847bad --- /dev/null +++ b/website/src/translations/zh/documentation/package/bun.md @@ -0,0 +1,7 @@ +# API `@deepkit/bun` + +```shell +npm install @deepkit/bun +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/core-rxjs.md b/website/src/translations/zh/documentation/package/core-rxjs.md new file mode 100644 index 000000000..bca3c9544 --- /dev/null +++ b/website/src/translations/zh/documentation/package/core-rxjs.md @@ -0,0 +1,7 @@ +# API `@deepkit/core-rxjs` + +```shell +npm install @deepkit/core-rxjs +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/core.md b/website/src/translations/zh/documentation/package/core.md new file mode 100644 index 000000000..b0e2ccdf0 --- /dev/null +++ b/website/src/translations/zh/documentation/package/core.md @@ -0,0 +1,7 @@ +# API `@deepkit/core` + +```shell +npm install @deepkit/core +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/devtool.md b/website/src/translations/zh/documentation/package/devtool.md new file mode 100644 index 000000000..4be73d4c3 --- /dev/null +++ b/website/src/translations/zh/documentation/package/devtool.md @@ -0,0 +1,5 @@ +# 开发工具 + +Deepkit 开发工具是一个 Chrome 插件,用于在 Chrome 开发者工具中调试 RPC 连接。 + +[Deepkit 开发工具](https://chromewebstore.google.com/detail/deepkit-devtool/lkncgbbafldohehlfdnkflbeapckdnlj) \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/event.md b/website/src/translations/zh/documentation/package/event.md new file mode 100644 index 000000000..7c137eaf5 --- /dev/null +++ b/website/src/translations/zh/documentation/package/event.md @@ -0,0 +1,9 @@ +# API `@deepkit/event` + +```shell +npm install @deepkit/event +``` + +阅读[应用事件](../app/events.md),了解在你的应用中如何使用事件的更多信息。 + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/filesystem-aws-s3.md b/website/src/translations/zh/documentation/package/filesystem-aws-s3.md new file mode 100644 index 000000000..942173a3b --- /dev/null +++ b/website/src/translations/zh/documentation/package/filesystem-aws-s3.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-aws-s3` + +```shell +npm install @deepkit/filesystem-aws-s3 +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/filesystem-database.md b/website/src/translations/zh/documentation/package/filesystem-database.md new file mode 100644 index 000000000..6de1d7fa2 --- /dev/null +++ b/website/src/translations/zh/documentation/package/filesystem-database.md @@ -0,0 +1,9 @@ +# API `@deepkit/filesystem-database` + +```shell +npm install @deepkit/filesystem-database +``` + +允许将所有受支持的 Deepkit ORM 数据库用作文件系统后端。 + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/filesystem-ftp.md b/website/src/translations/zh/documentation/package/filesystem-ftp.md new file mode 100644 index 000000000..d01521c0a --- /dev/null +++ b/website/src/translations/zh/documentation/package/filesystem-ftp.md @@ -0,0 +1,8 @@ +# API `@deepkit/filesystem-ftp` + +```shell +npm install @deepkit/filesystem-ftp +``` + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/filesystem-google.md b/website/src/translations/zh/documentation/package/filesystem-google.md new file mode 100644 index 000000000..7345966bd --- /dev/null +++ b/website/src/translations/zh/documentation/package/filesystem-google.md @@ -0,0 +1,8 @@ +# API `@deepkit/filesystem-google` + +```shell +npm install @deepkit/filesystem-google +``` + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/filesystem-sftp.md b/website/src/translations/zh/documentation/package/filesystem-sftp.md new file mode 100644 index 000000000..719ebfd37 --- /dev/null +++ b/website/src/translations/zh/documentation/package/filesystem-sftp.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem-sftp` + +```shell +npm install @deepkit/filesystem-sftp +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/filesystem.md b/website/src/translations/zh/documentation/package/filesystem.md new file mode 100644 index 000000000..68cc87b21 --- /dev/null +++ b/website/src/translations/zh/documentation/package/filesystem.md @@ -0,0 +1,7 @@ +# API `@deepkit/filesystem` + +```shell +npm install @deepkit/filesystem +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/framework.md b/website/src/translations/zh/documentation/package/framework.md new file mode 100644 index 000000000..533f18213 --- /dev/null +++ b/website/src/translations/zh/documentation/package/framework.md @@ -0,0 +1,8 @@ +# API `@deepkit/framework` + +```shell +npm install @deepkit/framework +``` + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/http.md b/website/src/translations/zh/documentation/package/http.md new file mode 100644 index 000000000..f96adb231 --- /dev/null +++ b/website/src/translations/zh/documentation/package/http.md @@ -0,0 +1,8 @@ +# API `@deepkit/http` + +```shell +npm install @deepkit/http +``` + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/injector.md b/website/src/translations/zh/documentation/package/injector.md new file mode 100644 index 000000000..b7b6febab --- /dev/null +++ b/website/src/translations/zh/documentation/package/injector.md @@ -0,0 +1,7 @@ +# API `@deepkit/injector` + +```shell +npm install @deepkit/injector +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/logger.md b/website/src/translations/zh/documentation/package/logger.md new file mode 100644 index 000000000..224a2be7c --- /dev/null +++ b/website/src/translations/zh/documentation/package/logger.md @@ -0,0 +1,7 @@ +# API `@deepkit/logger` + +```sh +npm install @deepkit/logger +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/mongo.md b/website/src/translations/zh/documentation/package/mongo.md new file mode 100644 index 000000000..63f027453 --- /dev/null +++ b/website/src/translations/zh/documentation/package/mongo.md @@ -0,0 +1,18 @@ +# API `@deepkit/mongo` + +```shell +npm install @deepkit/mongo +``` + +独立的 MongoDB 驱动以及用于 Deepkit ORM 的数据库适配器。 + +```typescript +import { MongoDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new MongoDatabaseAdapter('mongodb://localhost:27017/mydatabase'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/mysql.md b/website/src/translations/zh/documentation/package/mysql.md new file mode 100644 index 000000000..a972f372a --- /dev/null +++ b/website/src/translations/zh/documentation/package/mysql.md @@ -0,0 +1,17 @@ +# API `@deepkit/mysql` + +```shell +npm install @deepkit/mysql +``` + +```typescript +import { MySQLDatabaseAdapter } from '@deepkit/mysql'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('mysql://user:password@localhost/mydatabase'); +// const adapter = new MySQLDatabaseAdapter({host: 'localhost', port: 3306}); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/orm-browser.md b/website/src/translations/zh/documentation/package/orm-browser.md new file mode 100644 index 000000000..2cee213ce --- /dev/null +++ b/website/src/translations/zh/documentation/package/orm-browser.md @@ -0,0 +1,18 @@ +# Deepkit ORM 浏览器 + +```sh +npm install @deepkit/orm-browser +``` + +Deepkit ORM 浏览器是一个 Web 应用程序,可用于浏览数据库 ORM 模式、编辑内容、查看迁移变更以及为数据库填充种子数据。 + +它是[框架调试器](../framework.md)的一部分,但也可以单独使用。 + + + + + + + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/orm.md b/website/src/translations/zh/documentation/package/orm.md new file mode 100644 index 000000000..8ee0b58af --- /dev/null +++ b/website/src/translations/zh/documentation/package/orm.md @@ -0,0 +1,7 @@ +# API `@deepkit/orm` + +```shell +npm install @deepkit/orm +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/postgres.md b/website/src/translations/zh/documentation/package/postgres.md new file mode 100644 index 000000000..3f51e1f54 --- /dev/null +++ b/website/src/translations/zh/documentation/package/postgres.md @@ -0,0 +1,18 @@ +# API `@deepkit/postgres` + +```shell +npm install @deepkit/postgres +``` + +```typescript +import { PostgresDatabaseAdapter } from '@deepkit/mongo'; +import { Database } from '@deepkit/orm'; + +const adapter = new PostgresDatabaseAdapter('postgres://user:password@localhost/mydatabase'); +// const adapter = new PostgresDatabaseAdapter({ host: 'localhost', database: 'postgres', user: 'postgres' }); + +const database = new Database(adapter); +``` + + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/rpc-tcp.md b/website/src/translations/zh/documentation/package/rpc-tcp.md new file mode 100644 index 000000000..7a980d719 --- /dev/null +++ b/website/src/translations/zh/documentation/package/rpc-tcp.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc-tcp` + +```shell +npm install @deepkit/rpc-tcp +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/rpc.md b/website/src/translations/zh/documentation/package/rpc.md new file mode 100644 index 000000000..a583f5fe4 --- /dev/null +++ b/website/src/translations/zh/documentation/package/rpc.md @@ -0,0 +1,7 @@ +# API `@deepkit/rpc` + +```shell +npm install @deepkit/rpc +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/run.md b/website/src/translations/zh/documentation/package/run.md new file mode 100644 index 000000000..de6ffaf8b --- /dev/null +++ b/website/src/translations/zh/documentation/package/run.md @@ -0,0 +1,21 @@ +# API `@deepkit/run` + +```sh +npm install @deepkit/run +``` + +一种无需构建步骤即可运行 TypeScript 代码的简单方式。 + +此工具主要用于 Deepkit 自身的测试套件,但也可用于你自己的项目。 + +```typescript +import { typeOf } from '@deepkit/type'; + +console.log(typeOf()); +``` + +```sh +node --import @deepkit/run test.ts +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/sql.md b/website/src/translations/zh/documentation/package/sql.md new file mode 100644 index 000000000..3207c73ed --- /dev/null +++ b/website/src/translations/zh/documentation/package/sql.md @@ -0,0 +1,7 @@ +# API `@deepkit/sql` + +```shell +npm install @deepkit/sql +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/sqlite.md b/website/src/translations/zh/documentation/package/sqlite.md new file mode 100644 index 000000000..5486e2c82 --- /dev/null +++ b/website/src/translations/zh/documentation/package/sqlite.md @@ -0,0 +1,16 @@ +# API `@deepkit/sqlite` + +```shell +npm install @deepkit/sqlite +``` + +```typescript +import { SQLiteDatabaseAdapter } from '@deepkit/sqlite'; +import { Database } from '@deepkit/orm'; + +const adapter = new SQLiteDatabaseAdapter(':memory'); + +const database = new Database(adapter); +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/stopwatch.md b/website/src/translations/zh/documentation/package/stopwatch.md new file mode 100644 index 000000000..d2a69ed13 --- /dev/null +++ b/website/src/translations/zh/documentation/package/stopwatch.md @@ -0,0 +1,7 @@ +# API `@deepkit/stopwatch` + +```shell +npm install @deepkit/stopwatch +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/template.md b/website/src/translations/zh/documentation/package/template.md new file mode 100644 index 000000000..e6d908c2b --- /dev/null +++ b/website/src/translations/zh/documentation/package/template.md @@ -0,0 +1,7 @@ +# API `@deepkit/template` + +```shell +npm install @deepkit/template +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/topsort.md b/website/src/translations/zh/documentation/package/topsort.md new file mode 100644 index 000000000..cbeddd4c2 --- /dev/null +++ b/website/src/translations/zh/documentation/package/topsort.md @@ -0,0 +1,7 @@ +# API `@deepkit/topsort` + +```shell +npm install @deepkit/topsort +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/type-compiler.md b/website/src/translations/zh/documentation/package/type-compiler.md new file mode 100644 index 000000000..67db9ef1c --- /dev/null +++ b/website/src/translations/zh/documentation/package/type-compiler.md @@ -0,0 +1,9 @@ +# API `@deepkit/type-compiler` + +```shell +npm install @deepkit/type-compiler +``` + +TypeScript 转换器,用于在运行时提供运行时类型信息。 + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/type.md b/website/src/translations/zh/documentation/package/type.md new file mode 100644 index 000000000..195e08585 --- /dev/null +++ b/website/src/translations/zh/documentation/package/type.md @@ -0,0 +1,7 @@ +# API `@deepkit/type` + +```shell +npm install @deepkit/type +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/vite.md b/website/src/translations/zh/documentation/package/vite.md new file mode 100644 index 000000000..3f1bcdd0a --- /dev/null +++ b/website/src/translations/zh/documentation/package/vite.md @@ -0,0 +1,7 @@ +# API `@deepkit/vite` + +```shell +npm install @deepkit/vite +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/package/workflow.md b/website/src/translations/zh/documentation/package/workflow.md new file mode 100644 index 000000000..ff27914f7 --- /dev/null +++ b/website/src/translations/zh/documentation/package/workflow.md @@ -0,0 +1,7 @@ +# API `@deepkit/workflow` + +```shell +npm install @deepkit/workflow +``` + + \ No newline at end of file diff --git a/website/src/translations/zh/documentation/rpc.md b/website/src/translations/zh/documentation/rpc.md new file mode 100644 index 000000000..8c4f73b83 --- /dev/null +++ b/website/src/translations/zh/documentation/rpc.md @@ -0,0 +1,64 @@ +# RPC + +RPC(Remote Procedure Call,远程过程调用)使得远程服务器上的函数可以像本地函数一样被调用。与使用 HTTP 方法和 URL 进行映射的 HTTP 客户端-服务器通信不同,RPC 使用函数名进行映射。要发送的数据以普通函数参数的形式传递,服务器上函数调用的结果再发送回客户端。 + +RPC 的优点是客户端-服务器抽象较为轻量,因为它不涉及处理请求头、URL、查询字符串等。缺点是,通过 RPC 暴露的服务器函数不易被浏览器直接调用,通常需要特定的客户端。 + +RPC 的一个关键特性是客户端与服务器之间的数据会被自动序列化与反序列化。因此,通常可以实现类型安全的 RPC 客户端。一些 RPC 框架要求用户以特定格式提供类型(参数类型与返回类型)。这可能采用 DSL 的形式,例如 gRPC 的 Protocol Buffers、GraphQL,或 JavaScript 的 schema 构建器。RPC 框架也可能提供额外的数据校验,但并非所有框架都支持。 + +Deepkit RPC 能从 TypeScript 代码本身提取类型,因此无需使用代码生成器或手动定义。Deepkit 支持对参数与结果的自动序列化和反序列化。一旦在 Validation 中定义了额外的约束,它们会被自动校验。这使得通过 RPC 的通信具有极高的类型安全性与效率。Deepkit RPC 对基于 `rxjs` 的流式处理的支持,使其成为实时通信的理想工具。 + +为说明 RPC 背后的概念,请看以下代码: + +```typescript +//server.ts +class Controller { + hello(title: string): string { + return 'Hello ' + title + } +} +``` + +类似 hello 的方法在服务器端的类中与普通函数一样实现,可被远程客户端调用。 + +```typescript +//client.ts +const client = new RpcClient('localhost'); +const controller = client.controller(); + +const result = await controller.hello('World'); // => 'Hello World'; +``` + +由于 RPC 本质上基于异步通信,通信通常通过 HTTP 进行,也可以通过 TCP 或 WebSocket。这意味着在 TypeScript 中,所有函数调用都会被转换为 `Promise`。结果可通过相应的 `await` 异步获取。 + +## 同构 TypeScript + +当一个项目在客户端(通常为前端)与服务器端(后端)同时使用 TypeScript 时,称为同构 TypeScript。基于 TypeScript 类型的类型安全 RPC 框架对这类项目尤为有益,因为类型可以在客户端与服务器端之间共享。 + +为此,应将两端共用的类型抽取到单独的文件或包中。在各自的一端引入这些类型即可将其组合起来。 + +```typescript +//shared.ts +export class User { + id: number; + username: string; +} + +//server.ts +import { User } from './shared'; + +@rpc.controller('/user') +class UserController { + async getUser(id: number): Promise { + return await datbase.query(User).filter({id}).findOne(); + } +} + +//client.ts +import { UserControllerApi } from './shared'; +import type { UserController } from './server.ts' +const controller = client.controller('/user'); +const user = await controller.getUser(2); // => User +``` + +向后兼容可与普通本地 API 一样实现:要么将新参数标记为可选,要么添加一个新方法。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/rpc/dependency-injection.md b/website/src/translations/zh/documentation/rpc/dependency-injection.md new file mode 100644 index 000000000..f3d52a06b --- /dev/null +++ b/website/src/translations/zh/documentation/rpc/dependency-injection.md @@ -0,0 +1,55 @@ +# 依赖注入 + +控制器类由 `@deepkit/injector` 的依赖注入容器管理。在使用 Deepkit 框架时,这些控制器会自动访问声明该控制器的模块中的提供者。 + +在 Deepkit 框架中,控制器在依赖注入作用域 `rpc` 中被实例化,使所有控制器都能自动访问该作用域中的各种提供者。这些额外的提供者包括 `HttpRequest`(可选)、`RpcInjectorContext`、`SessionState`、`RpcKernelConnection` 和 `ConnectionWriter`。 + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { App } from '@deepkit/app'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +new App({ + providers: [{ provide: Database, useValue: new Database }] + controllers: [Controller], +}).run(); +``` + +然而,当手动实例化 `RpcKernel` 时,也可以传入一个 DI 容器。随后 RPC 控制器会通过该 DI 容器实例化。如果你希望在非 Deepkit 框架的环境(例如 Express.js)中使用 `@deepkit/rpc`,这将非常有用。 + +```typescript +import { RpcKernel, rpc } from '@deepkit/rpc'; +import { InjectorContext } from '@deepkit/injector'; +import { Database, User } from './database'; + +@rpc.controller('/main') +class Controller { + constructor(private database: Database) { + } + + @rpc.action() + async getUser(id: number): Promise { + return await this.database.query(User).filter({ id }).findOne(); + } +} + +const injector = InjectorContext.forProviders([ + Controller, + { provide: Database, useValue: new Database }, +]); +const kernel = new RpcKernel(injector); +kernel.registerController(Controller); +``` + +参见[依赖注入](../dependency-injection.md)以了解更多。 diff --git a/website/src/translations/zh/documentation/rpc/errors.md b/website/src/translations/zh/documentation/rpc/errors.md new file mode 100644 index 000000000..aa266a414 --- /dev/null +++ b/website/src/translations/zh/documentation/rpc/errors.md @@ -0,0 +1,55 @@ +# 错误 + +抛出的错误会连同其所有信息(如错误消息和堆栈跟踪)自动转发到客户端。 + +如果错误对象的名义实例很重要(因为你使用了 instanceof),则需要使用 `@entity.name('@error:unique-name')`,以便在运行时注册并复用给定的错误类。 + +```typescript +@entity.name('@error:myError') +class MyError extends Error {} + +// 服务器 +@rpc.controller('/main') +class Controller { + @rpc.action() + saveUser(user: User): void { + throw new MyError('Can not save user'); + } +} + +// 客户端 +// [MyError] 确保在运行时已知 MyError 类 +const controller = client.controller('/main', [MyError]); + +try { + await controller.getUser(2); +} catch (e) { + if (e instanceof MyError) { + // 哎呀,无法保存用户 + } else { + // 所有其他错误 + } +} +``` + +## 转换错误 + +由于抛出的错误会连同其所有信息(如错误消息和堆栈跟踪)自动转发到客户端,这可能会意外泄露敏感信息。要改变这一点,可以在 `transformError` 方法中对抛出的错误进行修改。 + +```typescript +class MyKernelSecurity extends RpcKernelSecurity { + constructor(private logger: Logger) { + super(); + } + + transformError(error: Error) { + // 包装为新的错误 + this.logger.error('Error in RPC', error); + return new Error('Something went wrong: ' + error.message); + } +} +``` + +请注意,一旦错误被转换为通用的 `error`,完整的堆栈跟踪和错误的身份将会丢失。因此,客户端无法对该错误使用 `instanceof` 检查。 + +如果 Deepkit RPC 用于两个微服务之间,因此客户端和服务器都由开发者完全控制,那么通常很少需要转换错误。另一方面,如果客户端在浏览器中运行且面向未知用户,则应在 `transformError` 中谨慎决定要披露的信息。若有不确定,应将每个错误都转换为通用的 `Error`,以确保不会泄露任何内部细节。在此情况下记录错误日志是个好主意。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/rpc/getting-started.md b/website/src/translations/zh/documentation/rpc/getting-started.md new file mode 100644 index 000000000..546aa6ab1 --- /dev/null +++ b/website/src/translations/zh/documentation/rpc/getting-started.md @@ -0,0 +1,146 @@ +# 入门 + +要使用 Deepkit RPC,必须正确安装 `@deepkit/type`,因为它基于运行时类型。参见[运行时类型安装](../runtime-types.md)。 + +成功完成后,即可安装 `@deepkit/rpc`,或安装已在底层使用该库的 Deepkit 框架。 + +```sh +npm install @deepkit/rpc +``` + +请注意,`@deepkit/rpc` 中的控制器类基于 TypeScript 装饰器,必须启用 experimentalDecorators 功能。 + +如果服务器和客户端各自有自己的 package.json,则必须在二者上都安装 `@deepkit/rpc` 包。 + +若要通过 TCP 与服务器通信,必须在客户端和服务器上安装 `@deepkit/rpc-tcp` 包。 + +```sh +npm install @deepkit/rpc-tcp +``` + +对于 WebSocket 通信,该包同样需要在服务器上使用。另一方面,浏览器端客户端使用的是官方标准中的 WebSocket。 + +如果客户端还需要在不支持 WebSocket 的环境(例如 NodeJS)中使用,则客户端需要安装 ws 包。 + +```sh +npm install ws +``` + +## 用法 + +下面是一个基于 WebSocket 和 @deepkit/rpc 低级 API 的完整示例。在使用 Deepkit 框架时,控制器通过应用模块提供,而无需手动实例化 RpcKernel。 + +_文件:server.ts_ + +```typescript +import { rpc, RpcKernel } from '@deepkit/rpc'; +import { RpcWebSocketServer } from '@deepkit/rpc-tcp'; + +@rpc.controller('/main') +export class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } +} + +const kernel = new RpcKernel(); +kernel.registerController(Controller); +const server = new RpcWebSocketServer(kernel, 'localhost:8081'); +server.start({ + host: '127.0.0.1', + port: 8081, +}); +console.log('Server started at ws://127.0.0.1:8081'); + +``` + +_文件:client.ts_ + +```typescript +import { RpcWebSocketClient } from '@deepkit/rpc'; +import type { Controller } from './server'; + +async function main() { + const client = new RpcWebSocketClient('ws://127.0.0.1:8081'); + const controller = client.controller('/main'); + + const result = await controller.hello('World'); + console.log('result', result); + + client.disconnect(); +} + +main().catch(console.error); + +``` + +## 服务器控制器 + +在远程过程调用(RPC)中,“Procedure”一词通常也称为“Action”。Action 是类中定义并使用 `@rpc.action` 装饰器标记的方法。类本身使用 `@rpc.controller` 装饰器标记为控制器并赋予唯一名称。该名称随后会在客户端中被引用,以定位到正确的控制器。可以按需定义并注册多个控制器。 + +```typescript +import { rpc } from '@deepkit/rpc'; + +@rpc.controller('/main'); +class Controller { + @rpc.action() + hello(title: string): string { + return 'Hello ' + title; + } + + @rpc.action() + test(): boolean { + return true; + } +} +``` + +只有标记为 `@rpc.action()` 的方法才能被客户端调用。 + +类型必须显式指定,不能通过推断获得。这一点很重要,因为序列化器需要确切知道类型的结构,才能将其转换为二进制数据(BSON)或 JSON,然后通过网络发送。 + +## 客户端控制器 + +在 RPC 的常规流程中,客户端可以在服务器上执行函数。但在 Deepkit RPC 中,服务器也可以在客户端上执行函数。为此,客户端也可以注册一个控制器。 + +待办 + +## 依赖注入 + +当使用 Deepkit 框架时,类由依赖注入容器实例化,从而自动可以访问应用中的所有其他提供者。 + +另请参见[依赖注入](dependency-injection.md#)。 + +## RxJS 流式处理 + +待办 + +## 名义类型 + +当客户端从函数调用中接收数据时,这些数据会先在服务器上序列化,然后在客户端反序列化。如果函数的返回类型包含类,这些类会在客户端被重建,但会丢失其名义身份和关联的方法。为了解决这个问题,请将这些类注册为具有唯一 ID/名称的名义类型。该方式应适用于 RPC-API 中使用的所有类。 + +要注册一个类,请使用装饰器 `@entity.name('id')`。 + +```typescript +import { entity } from '@deepkit/type'; + +@entity.name('user') +class User { + id!: number; + firstName!: string; + lastName!: string; + get fullName() { + return this.firstName + ' ' + this.lastName; + } +} +``` + +一旦该类被用作函数的返回结果,其身份将被保留。 + +```typescript +const controller = client.controller('/main'); + +const user = await controller.getUser(2); +user instanceof User; //当使用了 @entity.name 时为 true,否则为 false +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/rpc/security.md b/website/src/translations/zh/documentation/rpc/security.md new file mode 100644 index 000000000..3bf6656e2 --- /dev/null +++ b/website/src/translations/zh/documentation/rpc/security.md @@ -0,0 +1,131 @@ +# 安全 + +默认情况下,所有 RPC 函数都可以从任意客户端调用,并且点对点通信功能已启用。要精确控制哪些客户端被允许执行哪些操作,可以重写 `RpcKernelSecurity` 类。 + +```typescript +import { RpcKernelSecurity, Session, RpcControllerAccess } from '@deepkit/type'; + +//包含默认实现 +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + return true; + } + + async isAllowedToRegisterAsPeer(session: Session, peerId: string): Promise { + return true; + } + + async isAllowedToSendToPeer(session: Session, peerId: string): Promise { + return true; + } + + async authenticate(token: any): Promise { + throw new Error('Authentication not implemented'); + } + + transformError(err: Error) { + return err; + } +} +``` + +要使用它,将该提供者传递给 `RpcKernel`: + +```typescript +const kernel = new RpcKernel([{provide: RpcKernelSecurity, useClass: MyKernelSecurity, scope: 'rpc'}]); +``` + +或者,对于 Deepkit 应用,可在应用中使用提供者覆盖 `RpcKernelSecurity` 类: + +```typescript +import { App } from '@deepkit/type'; +import { RpcKernelSecurity } from '@deepkit/rpc'; +import { FrameworkModule } from '@deepkit/framework'; + +new App({ + controllers: [MyRpcController], + providers: [ + {provide: RpcKernelSecurity, useClass: MyRpcKernelSecurity, scope: 'rpc'} + ], + imports: [new FrameworkModule] +}).run(); +``` + +## 认证 / 会话 + +默认情况下,`Session` 对象是匿名会话,这意味着客户端尚未认证。当客户端想要进行认证时,会调用 `authenticate` 方法。`authenticate` 方法接收到的 token 来自客户端,且可以是任意值。 + +一旦客户端设置了 token,认证会在首次调用 RPC 函数时或手动调用 `client.connect()` 时执行。 + +```typescript +const client = new RpcWebSocketClient('localhost:8081'); +client.token.set('123456789'); + +const controller = client.controller('/main'); +``` + +在这种情况下,`RpcKernelSecurity.authenticate` 会收到 token `123456789`,并可相应返回不同的会话。随后,返回的会话会传递给其他所有方法,例如 `hasControllerAccess`。 + +```typescript +import { Session, RpcKernelSecurity } from '@deepkit/rpc'; + +class UserSession extends Session { +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.controllerClassType instanceof MySecureController) { + //MySecureController 需要 UserSession + return session instanceof UserSession; + } + return true; + } + + async authenticate(token: any): Promise { + if (token === '123456789') { + //username 可以是一个 ID 或用户名 + return new UserSession('username', token); + } + throw new Error('Authentication failed'); + } +} +``` + +## 控制器访问 + +`hasControllerAccess` 方法决定客户端是否被允许执行特定的 RPC 函数。该方法在每次 RPC 函数调用时都会被调用。如果返回 `false`,则拒绝访问,并在客户端抛出错误。 + +`RpcControllerAccess` 包含有关该 RPC 函数的有用信息: + +```typescript +interface RpcControllerAccess { + controllerName: string; + controllerClassType: ClassType; + actionName: string; + actionGroups: string[]; + actionData: { [name: string]: any }; +} +``` + +可以通过装饰器 `@rpc.action()` 修改分组和附加数据: + +```typescript +class Controller { + @rpc.action().group('secret').data('role', 'admin') + saveUser(user: User): void { + } +} + +class MyKernelSecurity extends RpcKernelSecurity { + async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise { + if (controllerAccess.actionGroups.includes('secret')) { + if (session instanceof UserSession) { + //待办:检查 + return session.username === 'admin'; + } + return false; + } + return true; + } +} +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/rpc/transport.md b/website/src/translations/zh/documentation/rpc/transport.md new file mode 100644 index 000000000..14fe0135c --- /dev/null +++ b/website/src/translations/zh/documentation/rpc/transport.md @@ -0,0 +1,17 @@ +# 传输协议 + +Deepkit RPC 支持多种传输协议。WebSocket 协议兼容性最好(因为浏览器支持),同时支持诸如流式传输等全部特性。TCP 通常更快,适合服务器(微服务)之间或非浏览器客户端的通信。但用于服务器到服务器的通信,WebSocket 也同样表现良好。 + +## HTTP + +Deepkit 的 RPC HTTP 协议是一种在浏览器中特别容易调试的变体,因为每个函数调用都是一个 HTTP 请求,但它也有局限,例如不支持 RxJS 流式传输。 + +待办:尚未实现。 + +## WebSocket + +@deepkit/rpc-tcp `RpcWebSocketServer`,以及浏览器 WebSocket 或 Node 的 `ws` 包。 + +## TCP + +@deepkit/rpc-tcp 的 `RpcNetTcpServer` 和 `RpcNetTcpClientAdapter` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types.md b/website/src/translations/zh/documentation/runtime-types.md new file mode 100644 index 000000000..12b6136c2 --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types.md @@ -0,0 +1,9 @@ +# 运行时类型 + +TypeScript 中的运行时类型信息解锁了此前不可用或需要变通实现的新工作流与功能。现代开发过程高度依赖为 GraphQL、验证器、ORM,以及诸如 ProtoBuf 之类的编码器声明类型和模式。这些工具可能要求开发者学习与其用例相关的专用语言,例如 ProtoBuf 和 GraphQL 拥有各自的声明语言,或验证器使用自己的模式 API 或 JSON‑Schema。 + +TypeScript 已经足够强大,可以描述复杂结构,甚至完全替代像 GraphQL、ProtoBuf 和 JSON‑Schema 这样的声明格式。借助运行时类型系统,可以在不使用任何代码生成器或诸如“Zod”这类运行时 JavaScript 类型声明库的情况下覆盖这些工具的用例。Deepkit 库旨在提供运行时类型信息,帮助更轻松地开发高效且兼容的解决方案。 + +Deepkit 构建在运行时读取类型信息的能力之上,并尽可能利用 TypeScript 的类型信息以提高效率。该运行时类型系统允许读取和计算动态类型,例如类属性、函数参数和返回类型。Deepkit 挂接到 TypeScript 的编译过程,通过使用[自定义字节码和虚拟机](https://github.com/microsoft/TypeScript/issues/47658)确保所有类型信息被嵌入到生成的 JavaScript 中,使开发者能够以编程方式访问类型信息。 + +借助 Deepkit,开发者可以在运行时使用现有的 TypeScript 类型进行验证、序列化等,从而简化开发流程并提升工作效率。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/bytecode.md b/website/src/translations/zh/documentation/runtime-types/bytecode.md new file mode 100644 index 000000000..b8b5a52b7 --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/bytecode.md @@ -0,0 +1,76 @@ +# 字节码 + +为了详细了解 Deepkit 如何在 JavaScript 中编码和读取类型信息,本章将进行说明。它解释了类型如何实际转换为字节码、如何在 JavaScript 中发出,以及如何在运行时进行解释。 + +## Typen-Compiler + +类型编译器(位于 @deepkit/type-compiler)负责读取 TypeScript 文件中定义的类型,并将其编译为字节码。该字节码包含在运行时执行类型所需的一切。 +截至本文撰写时,类型编译器是所谓的 TypeScript transformer。此转换器是 TypeScript 编译器本身的一个插件,会将一个 TypeScript AST(抽象语法树)转换为另一个 TypeScript AST。Deepkit 的类型编译器在此过程中读取 AST,生成相应的字节码,并将其插入到 AST 中。 + +TypeScript 本身不允许通过 tsconfig.json 配置该插件(即 transformer)。要么需要直接使用 TypeScript 编译器 API,要么使用像 Webpack 搭配 `ts-loader` 这样的构建系统。为避免给 Deepkit 用户带来这种不便,只要安装了 `@deepkit/type-compiler`,Deepkit 的类型编译器就会自动安装到 `node_modules/typescript` 中。这样,所有访问本地安装 TypeScript(即 `node_modules/typescript` 中的那个)的构建工具都会自动启用类型编译器。由此,tsc、Angular、webpack、ts-node 以及其他一些工具都可以自动与 Deepkit 的类型编译器协同工作。 + +如果未启用 NPM 安装脚本的自动运行,从而没有修改本地安装的 typescript,则需要按需手动运行该过程。或者,也可以在诸如 webpack 之类的构建工具中手动使用类型编译器。参见上文的安装章节。 + +## 字节码编码 + +字节码是一组为虚拟机准备的指令序列,并在 JavaScript 中编码为由引用和字符串(实际字节码)组成的数组。 + +```typescript +// TypeScript +type TypeA = string; + +// 生成的 JavaScript +const typeA = ['&']; +``` + +已有的指令每条占用一个字节,可以在 `@deepkit/type-spec` 中找到,作为 `ReflectionOp` 枚举。截至本文撰写时,指令集包含 81 条以上的指令。 + +```typescript +enum ReflectionOp { + never, + any, + unknown, + void, + object, + + string, + number, + + // ... 还有很多 +} +``` + +为节省内存,指令序列被编码为字符串。因此,类型 `string[]` 可以抽象为字节码程序 `[string, array]`,其字节为 `[5, 37]`,并使用如下算法进行编码: + +```typescript +function encodeOps(ops: ReflectionOp[]): string { + return ops.map(v => String.fromCharCode(v + 33)).join(''); +} +``` + +相应地,5 变成字符 `&`,37 变成字符 `F`。合在一起就是 `&F`,并作为 `['&F']` 在 JavaScript 中输出。 + +```typescript +// TypeScript +export type TypeA = string[]; + +// 生成的 JavaScript +export const __ΩtypeA = ['&F']; +``` + +为防止命名冲突,每个类型都会被赋予一个“_Ω”前缀。对于每个显式定义且被导出、或被某个导出类型使用的类型,都会在 JavaScript 中发出对应的字节码。类和函数也会直接以属性的形式携带字节码。 + +```typescript +// TypeScript +function log(message: string): void {} + +// 生成的 JavaScript +function log(message) {} +log.__type = ['message', 'log', 'P&2!$/"']; +``` + +## 虚拟机 + +在运行时,虚拟机(`@deepkit/type` 中的 Processor 类)负责解码并执行已编码的字节码。它始终返回一个类型对象,如[反射 API](./reflection.md)中所述。 + +更多信息参见[TypeScript 字节码解释器 / 运行时类型 #47658](https://github.com/microsoft/TypeScript/issues/47658)。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/custom-serializer.md b/website/src/translations/zh/documentation/runtime-types/custom-serializer.md new file mode 100644 index 000000000..8d7c4efed --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/custom-serializer.md @@ -0,0 +1,99 @@ +# 自定义序列化器 + +默认情况下,`@deepkit/type` 自带 JSON 序列化器以及对 TypeScript 类型的类型校验。你可以扩展它,添加或移除序列化功能,或改变校验的方式,因为校验同样与序列化器相关联。 + +## 新建序列化器 + +序列化器只是一个带有已注册序列化模板的 `Serializer` 类实例。序列化模板是为 JIT 序列化过程生成 JavaScript 代码的小函数。对于每种类型(String、Number、Boolean 等),都有一个单独的序列化器模板,负责返回用于数据转换或校验的代码。该代码必须与用户使用的 JavaScript 引擎兼容。 + +只有在模板函数执行期间,你(或者说应当)才能完全访问到该类型的完整信息。其理念是将类型转换所需的全部信息直接嵌入到生成的 JavaScript 代码中,从而得到高度优化的代码(也称为 JIT 优化代码)。 + +下面的示例创建一个空的序列化器。 + +```typescript +import { EmptySerializer } from '@deepkit/type'; + +class User { + name: string = ''; + created: Date = new Date; +} + +const mySerializer = new EmptySerializer('mySerializer'); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 0 } +``` + +如你所见,没有发生任何转换(`created` 仍然是数字,但我们将其定义为 `Date`)。要改变这一点,我们为 Date 类型的反序列化注册一个序列化器模板。 + +```typescript +mySerializer.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); +}); + +const user = deserialize({ name: 'Peter', created: 0 }, undefined, mySerializer); +console.log(user); +``` + +```sh +$ ts-node app.ts +User { name: 'Peter', created: 2021-06-10T19:34:27.301Z } +``` + +现在我们的序列化器会将该值转换为 Date 对象。 + +要在序列化时做同样的事,我们再注册一个序列化模板。 + +```typescript +mySerializer.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); +}); + +const user1 = new User(); +user1.name = 'Peter'; +user1.created = new Date('2021-06-10T19:34:27.301Z'); +console.log(serialize(user1, undefined, mySerializer)); +``` + +```sh +{ name: 'Peter', created: '2021-06-10T19:34:27.301Z' } +``` + +我们新的序列化器现在会在序列化过程中正确地将 Date 对象的日期转换为字符串。 + +## 示例 + +想查看更多示例,可以看看 Deepkit Type 中包含的[JSON 序列化器](https://github.com/deepkit/deepkit-framework/blob/master/packages/type/src/serializer.ts#L1688)的代码。 + +## 扩展现有序列化器 + +如果你想扩展一个现有的序列化器,可以使用类继承。这之所以可行,是因为序列化器应当在构造函数中注册它们的模板。 + +```typescript +class MySerializer extends Serializer { + constructor(name: string = 'mySerializer') { + super(name); + this.registerTemplates(); + } + + protected registerTemplates() { + this.deserializeRegistry.register(ReflectionKind.string, (type, state) => { + state.addSetter(`String(${state.accessor})`); + }); + + this.deserializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`new Date(${state.accessor})`); + }); + + this.serializeRegistry.registerClass(Date, (type, state) => { + state.addSetter(`${state.accessor}.toJSON()`); + }); + } +} +const mySerializer = new MySerializer(); +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/extend.md b/website/src/translations/zh/documentation/runtime-types/extend.md new file mode 100644 index 000000000..2f4b90672 --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/extend.md @@ -0,0 +1,56 @@ +# 扩展 + +## 自定义序列化 + +你可以通过编写你自己的 `Serializer` 或扩展默认的 `serializer` 来扩展某个类型的序列化。 + +下面的示例展示了如何将类 `Point` 序列化/反序列化为元组 `[number, number]`。 + +```typescript +import { serializer, SerializationError } from '@deepkit/type'; + +class Point { + constructor(public x: number, public y: number) { + } +} + +// 反序列化表示从 JSON 到(类)实例。 +serializer.deserializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: any) => { + // 如果它已经是 Point 实例,那就完成了 + if (v instanceof Point) return v; + + // 此时 `v` 可能是任意值(除了 undefined),因此需要进行检查 + if (!Array.isArray(v)) throw new SerializationError('Expected array'); + if (v.length !== 2) throw new SerializationError('Expected array with two elements'); + if (typeof v[0] !== 'number' || typeof v[1] !== 'number') throw new SerializationError('Expected array with two numbers'); + return new Point(v[0], v[1]); + }); +}); + +serializer.serializeRegistry.registerClass(Point, (type, state) => { + state.convert((v: Point) => { + // 此时 `v` 一定是 Point 实例 + return [v.x, v.y]; + }); +}); + +// cast 和 deserialize 默认使用 `serializer` +const point = cast([1, 2], undefined, serializer); +expect(point).toBeInstanceOf(Point); +expect(point.x).toBe(1); +expect(point.y).toBe(2); + +{ + expect(() => deserialize(['vbb'])).toThrowError(SerializationError); + expect(() => deserialize(['vbb'])).toThrow('Expected array with two elements') +} + +// serialize 默认使用 `serializer` +const json = serialize(point); +expect(json).toEqual([1, 2]); +``` + +请注意,这仅适用于常规的 `@deepkit/type` 函数,例如 `cast`、`deserialize` 和 `serialize`。 + +这不会被传递到数据库层,因为数据库层在迁移和序列化时使用实体类中定义的类型(例如 BSON 序列化)。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/external-types.md b/website/src/translations/zh/documentation/runtime-types/external-types.md new file mode 100644 index 000000000..fdce35319 --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/external-types.md @@ -0,0 +1,49 @@ +# 外部类型 + +## 外部类 + +由于 TypeScript 默认不包含类型信息,从其他包导入的类型/类(未使用 @deepkit/type-compiler 的)将无法获得类型信息。 + +要为外部类注解类型,请使用 `annotateClass`,并确保该函数在应用程序的引导阶段执行,在导入的类被用于其他地方之前。 + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +interface AnnotatedClass { + id: number; + title: string; +} + +annotateClass(MyExternalClass); + +//现在所有对 MyExternalClass 的使用都会返回 AnnotatedClass 的类型 +serialize({...}); + +//MyExternalClass 现在也可以用于其他类型中 +interface User { + id: number; + clazz: MyExternalClass; +} +``` + +现在可以在序列化函数和反射 API 中使用 `MyExternalClass`。 + +以下演示如何为泛型类添加注解: + +```typescript +import { MyExternalClass } from 'external-package'; +import { annotateClass } from '@deepkit/type'; + +class AnnotatedClass { + id!: T; +} + +annotateClass(ExternalClass, AnnotatedClass); +``` + +## 导入类型 + +TypeScript 设计的 `import type` 语法可避免导入实际的 JavaScript 代码,仅用于类型检查。这在以下情况很有用:当你想使用一个仅在编译时可用、运行时不可用的包中的类型,或者你不想在运行时实际加载该包时。 + +Deepkit 遵循 `import type` 的理念,不会生成任何运行时代码。这意味着如果你使用 `import type`,在运行时将无法获得类型信息。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/getting-started.md b/website/src/translations/zh/documentation/runtime-types/getting-started.md new file mode 100644 index 000000000..a15caf90d --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/getting-started.md @@ -0,0 +1,105 @@ +# 入门 + +要安装 Deepkit 的运行时类型系统,需要两个包:Deepkit Type 编译器和 Deepkit Type 包本身。类型编译器是一个 TypeScript 变换器,用于从 TypeScript 类型生成运行时类型信息。Type 包包含运行时虚拟机和类型注解,以及许多用于处理类型的实用函数。 + +## 安装 + +```sh +npm install --save @deepkit/type +npm install --save-dev @deepkit/type-compiler typescript ts-node +``` + +默认不会生成运行时类型信息。必须在 `tsconfig.json` 文件中设置 `"reflection": true` 才能启用。 + +如果要使用装饰器,必须在 `tsconfig.json` 中启用 `"experimentalDecorators": true`。这对使用 `@deepkit/type` 并非绝对必要,但对其他 Deepkit 库的某些功能以及 Deepkit 框架是必要的。 + +_文件:tsconfig.json_ + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6", + "moduleResolution": "node", + "experimentalDecorators": true + }, + "reflection": true +} +``` + +编写你的第一段带有运行时类型信息的代码: + +_文件:app.ts_ + +```typescript +import { cast, MinLength, ReflectionClass } from '@deepkit/type'; + +interface User { + username: string & MinLength<3>; + birthDate?: Date; +} + +const user = cast({ + username: 'Peter', + birthDate: '2010-10-10T00:00:00Z' +}); +console.log(user); + +const reflection = ReflectionClass.from(); +console.log(reflection.getProperty('username').type); +``` + +使用 `ts-node` 运行它: + +```sh +./node_modules/.bin/ts-node app.ts +``` + +## 交互式示例 + +这是一个 codesandbox 示例: https://codesandbox.io/p/sandbox/deepkit-runtime-types-fjmc2f?file=index.ts + +## 类型编译器 + +TypeScript 本身不允许通过 `tsconfig.json` 配置类型编译器。必须直接使用 TypeScript 编译器 API,或者使用像 Webpack 配合 _ts-loader_ 这样的构建系统。为避免这种不便,Deepkit 类型编译器在安装 `@deepkit/type-compiler` 后会自动将自身安装到 `node_modules/typescript` 中(这是通过 NPM 的 install hooks 完成的)。 +这使得所有访问本地安装的 TypeScript(即 `node_modules/typescript` 中的那个)的构建工具都能自动启用类型编译器。这使得 _tsc_、Angular、webpack、_ts-node_ 和其他一些工具能够自动与 Deepkit 类型编译器一起工作。 + +如果类型编译器未能成功自动安装(例如因为 NPM install hooks 被禁用),可以使用以下命令手动完成: + +```sh +node_modules/.bin/deepkit-type-install +``` + +请注意,如果本地 typescript 版本已更新(例如 package.json 中的 typescript 版本发生了变化并运行了 `npm install`),必须运行 `deepkit-type-install`。 + +## Webpack + +如果你希望在 webpack 构建中使用类型编译器,可以使用 `ts-loader` 包(或任何支持注册 transformer 的 TypeScript loader)。 + +_文件:webpack.config.js_ + +```javascript +const typeCompiler = require('@deepkit/type-compiler'); + +module.exports = { + entry: './app.ts', + module: { + rules: [ + { + test: /\.tsx?$/, + use: { + loader: 'ts-loader', + options: { + // 这将启用 @deepkit/type 的类型编译器 + getCustomTransformers: (program, getProgram) => ({ + before: [typeCompiler.transformer], + afterDeclarations: [typeCompiler.declarationTransformer], + }), + } + }, + exclude: /node_modules/, + }, + ], + }, +} +``` \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/reflection.md b/website/src/translations/zh/documentation/runtime-types/reflection.md new file mode 100644 index 000000000..420e8f8f3 --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/reflection.md @@ -0,0 +1,288 @@ +# 反射 + +若要直接处理类型信息本身,有两种基本变体:类型对象和反射类。类型对象是由 `typeOf()` 返回的常规 JS 对象。反射类将在下文讨论。 + +函数 `typeOf` 适用于所有类型,包括接口、对象字面量、类、函数和类型别名。它返回一个包含该类型全部信息的类型对象。你可以传入任意类型作为类型参数,包括泛型。 + +```typescript +import { typeOf } from '@deepkit/type'; + +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} + +typeOf<{id: number}>(); //{kind: 4, types: [{kind: 6, name: 'id'}]} + +class User { + id: number +} + +typeOf(); //{kind: 4, types: [...]} + +function test(id: number): string {} + +typeOf(); //{kind: 12, parameters: [...], return: {kind: 5}} +``` + +类型对象是一个简单的对象字面量,包含一个 `kind` 属性用来指示该类型对象的类型。`kind` 属性是一个数字,其含义取自枚举 `ReflectionKind`。`ReflectionKind` 在 `@deepkit/type` 包中定义如下: + +```typescript +enum ReflectionKind { + never, //0 + any, //1 + unknown, //2 + void, //3 + object, //4 + string, //5 + number, //6 + boolean, //7 + symbol, //8 + bigint, //9 + null, //10 + undefined, //11 + + //... 以及更多 +} +``` + +可能返回的类型对象有很多种。最简单的有 `never`、`any`、`unknown`、`void`、`null` 和 `undefined`,它们表示如下: + +```typescript +{kind: 0}; //never +{kind: 1}; //any +{kind: 2}; //unknown +{kind: 3}; //void +{kind: 10}; //null +{kind: 11}; //undefined +``` + +例如,数字 0 是 `ReflectionKind` 枚举的第一个条目,此处为 `never`;数字 1 是第二个条目,此处为 `any`,依此类推。相应地,像 `string`、`number`、`boolean` 这样的原始类型表示如下: + +```typescript +typeOf(); //{kind: 5} +typeOf(); //{kind: 6} +typeOf(); //{kind: 7} +``` + +这些较为简单的类型在类型对象上没有更多信息,因为它们是直接作为类型参数传递给 `typeOf` 的。然而,如果通过类型别名传递类型,则可以在类型对象上找到更多信息。 + +```typescript +type Title = string; + +typeOf(); //{kind: 5, typeName: 'Title'} +``` + +在此情况下,类型别名 'Title' 的名称也可用。如果类型别名是泛型,所传入的类型也会在类型对象上可用。 + +```typescript +type Title<T> = T extends true ? string : number; + +typeOf<Title<true>>(); +{kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]} +``` + +如果传入的类型是索引访问操作符的结果,则会包含容器和索引类型: + +```typescript +interface User { + id: number; + username: string; +} + +typeOf<User['username']>(); +{kind: 5, indexAccessOrigin: { + container: {kind: Reflection.objectLiteral, types: [...]}, + Index: {kind: Reflection.literal, literal: 'username'} +}} +``` + +接口和对象字面量都以 Reflection.objectLiteral 输出,并在 `types` 数组中包含属性和方法。 + +```typescript +interface User { + id: number; + username: string; + login(password: string): void; +} + +typeOf<User>(); +{ + kind: Reflection.objectLiteral, + types: [ + {kind: Reflection.propertySignature, name: 'id', type: {kind: 6}}, + {kind: Reflection.propertySignature, name: 'username', + type: {kind: 5}}, + {kind: Reflection.methodSignature, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} + +type User = { + id: number; + username: string; + login(password: string): void; +} +typeOf<User>(); //返回与上面相同的对象 +``` + +索引签名也在 `types` 数组中。 + +```typescript +interface BagOfNumbers { + [name: string]: number; +} + + +typeOf<BagOfNumbers>; +{ + kind: Reflection.objectLiteral, + types: [ + { + kind: Reflection.indexSignature, + index: {kind: 5}, //字符串 + type: {kind: 6}, //数字 + } + ] +} + +type BagOfNumbers = { + [name: string]: number; +} +typeOf<BagOfNumbers>(); //返回与上面相同的对象 +``` + +类与对象字面量类似,除了 `classType`(它是对类本身的引用)外,也在 `types` 数组下拥有其属性和方法。 + +```typescript +class User { + id: number = 0; + username: string = ''; + login(password: string): void { + //什么也不做 + } +} + +typeOf<User>(); +{ + kind: Reflection.class, + classType: User, + types: [ + {kind: Reflection.property, name: 'id', type: {kind: 6}}, + {kind: Reflection.property, name: 'username', + type: {kind: 5}}, + {kind: Reflection.method, name: 'login', parameters: [ + {kind: Reflection.parameter, name: 'password', type: {kind: 5}} + ], return: {kind: 3}}, + ] +} +``` + +请注意,Reflection.propertySignature 的类型变为了 Reflection.property,而 Reflection.methodSignature 变为了 Reflection.method。由于类上的属性和方法具有附加的属性,这些信息也可以被检索。后者还包括 `visibility`、`abstract` 和 `default`。 +类的类型对象仅包含类自身的属性和方法,而不包含父类的。这与接口/对象字面量的类型对象相反,后者会将所有父级的属性签名和方法签名解析到 `types` 中。若要解析父类的属性和方法,可以使用 ReflectionClass 及其 `ReflectionClass.getProperties()`(见下节),或使用 `@deepkit/type` 的 `resolveTypeMembers()`。 + +类型对象种类繁多。例如,字面量、模板字面量、Promise、枚举、联合、数组、元组等。要了解都有哪些以及可用的信息,建议从 `@deepkit/type` 导入 `Type`。它是一个包含所有可能子类型的 `union`,如 TypeAny、TypeUnknonwn、TypeVoid、TypeString、TypeNumber、TypeObjectLiteral、TypeArray、TypeClass 等。在那里你可以找到精确的结构。 + +## 类型缓存 + +当未传递泛型参数时,类型对象会对类型别名、函数和类进行缓存。这意味着调用 `typeOf<MyClass>()` 总是返回相同的对象。 + +```typescript +type MyType = string; + +typeOf<MyType>() === typeOf<MyType>(); //true +``` + +然而,一旦使用了泛型类型,即便每次传入的类型都相同,也总会创建新对象。这是因为理论上可能存在无限多的组合,这样的缓存将会导致内存泄漏。 + +```typescript +type MyType<T> = T; + +typeOf<MyType<string>>() === typeOf<MyType<string>>(); +//false +``` + +但是,一旦某个类型在递归类型中被多次实例化,它会被缓存。不过,该缓存的持续时间仅限于该类型计算的时刻,之后即不存在。此外,尽管类型对象被缓存了,返回的是新的引用,并不是完全相同的对象。 + +```typescript +type MyType<T> = T; +type Object = { + a: MyType<string>; + b: MyType<string>; +}; + +typeOf<Object>(); +``` + +在计算 `Object` 期间,`MyType<string>` 会被缓存。因此,`a` 和 `b` 的 PropertySignature 具有来自缓存的相同 `type`,但它们不是同一个 Type 对象。 + +所有非根类型对象都有一个 parent 属性,通常指向封闭的父级。例如,这对于判断某个类型是否属于联合类型的一部分很有用。 + +```typescript +type ID = string | number; + +typeOf<ID>(); +*Ref 1* { + kind: ReflectionKind.union, + types: [ + {kind: ReflectionKind.string, parent: *Ref 1* } } + {kind: ReflectionKind.number, parent: *Ref 1* } + ] +} +``` + +“Ref 1” 指向实际的联合类型对象。 + +对于如上示例所示的已缓存类型对象,`parent` 属性并不总是真正的父级。例如,对于被多次使用的类,尽管 `types` 中的直接类型(TypePropertySignature 和 TypeMethodSignature)指向正确的 TypeClass,但这些签名类型的 `type` 会指向缓存条目的 TypeClass 的签名类型。了解这一点很重要,以免无限地读取父级结构,而只读取直接父级。父级不具备无限精确性的原因是出于性能考虑。 + +## JIT 缓存 + +在后续描述的一些函数和特性中,常常基于类型对象。为了高效地实现其中一些,需要为每个类型对象提供一个 JIT(just in time)缓存。这可以通过 `getJitContainer(type)` 提供。该函数返回一个简单对象,可在其上存储任意数据。只要不再持有对该对象的引用,当类型对象本身也不再被引用时,它将由 GC 自动删除。 + +## 反射类 + +除了 `typeOf<>()` 函数外,还有各种反射类,它们为类型对象提供了面向对象(OOP)的替代方案。反射类仅适用于类、接口/对象字面量和函数及其直接子类型(属性、方法、参数)。更深层的类型必须再次通过类型对象读取。 + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + + +const reflection = ReflectionClass.from<User>(); + +reflection.getProperties(); //[ReflectionProperty, ReflectionProperty] +reflection.getProperty('id'); //ReflectionProperty + +reflection.getProperty('id').name; //'id' +reflection.getProperty('id').type; //{kind: ReflectionKind.number} +reflection.getProperty('id').isOptional(); //false +``` + +## 接收类型信息 + +为了提供对类型进行操作的函数,向用户提供手动传入类型的能力是很有用的。例如,在一个校验函数中,可能希望将目标类型作为第一个类型参数传入,并将要校验的数据作为第一个函数参数。 + +```typescript +validate<string>(1234); +``` + +为了让此函数获取到类型 `string`,必须把这一点告知类型编译器。 + +```typescript +function validate<T>(data: any, type?: ReceiveType<T>): void; +``` + +带有对第一个类型参数 `T` 的引用的 `ReceiveType` 会向类型编译器发出信号,表示每次调用 `validate` 都应把该类型放在第二个位置(因为 `type` 在第二个位置声明)。随后在运行时读取该信息,使用 `resolveReceiveType` 函数。 + +```typescript +import { resolveReceiveType, ReceiveType } from '@deepkit/type'; + +function validate<T>(data: any, type?: ReceiveType<T>): void { + type = resolveReceiveType(type); +} +``` + +将结果赋值给同一个变量是有益的,以避免不必要地创建新变量。现在 `type` 中要么存储着一个类型对象,要么会抛出错误,例如未传入类型参数、Deepkit 的类型编译器未正确安装,或未启用类型信息的发出(参见上文安装章节)。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/serialization.md b/website/src/translations/zh/documentation/runtime-types/serialization.md new file mode 100644 index 000000000..f8c07a93b --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/serialization.md @@ -0,0 +1,153 @@ +# 序列化 + +序列化是将数据类型转换为适合传输或存储等用途的格式的过程。反序列化则是其逆过程。这个过程是无损的,意味着数据在与序列化目标之间来回转换时不会丢失数据类型信息或数据本身。 + +在 JavaScript 中,序列化通常发生在 JavaScript 对象与 JSON 之间。JSON 仅支持 String、Number、Boolean、Object 和 Array。而 JavaScript 则支持许多其他类型,如 BigInt、ArrayBuffer、typed arrays、Date、自定义类实例等。要使用 JSON 将 JavaScript 数据传输到服务器,你需要一个序列化过程(在客户端)以及一个反序列化过程(在服务器端);反之亦然,如果服务器将数据以 JSON 发送给客户端。仅使用 `JSON.parse` 和 `JSON.stringify` 往往不足以实现这一点,因为它不是无损的。 + +对于非平凡的数据,这种序列化过程是绝对必要的,因为 JSON 即使对于诸如日期这样的基本类型也会丢失信息。一个新的 Date 最终在 JSON 中会被序列化为字符串: + +```typescript +const json = JSON.stringify(new Date); +//'"2022-05-13T20:48:51.025Z" +``` + +如你所见,JSON.stringify 的结果是一个 JSON 字符串。如果再用 JSON.parse 进行反序列化,你得到的不会是一个 date 对象,而是一个字符串。 + +```typescript +const value = JSON.parse('"2022-05-13T20:48:51.025Z"'); +//"2022-05-13T20:48:51.025Z" +``` + +尽管有各种变通方法可以让 JSON.parse 反序列化 Date 对象,但它们容易出错且性能较差。为了在此场景以及许多其他类型上实现类型安全的序列化和反序列化,就需要一个序列化过程。 + +可用的主要函数有四个:`serialize`、`cast`、`deserialize` 和 `validatedDeserialize`。在这些函数的底层,默认使用的是来自 `@deepkit/type` 的全局可用 JSON 序列化器,但也可以使用自定义的序列化目标。 + +Deepkit Type 支持用户自定义序列化目标,但已经内置了一个强大的 JSON 序列化目标,能够将数据序列化为 JSON 对象,并可通过 JSON.stringify 正确且安全地转换为 JSON。配合 `@deepkit/bson`,还可以使用 BSON 作为序列化目标。如何创建自定义的序列化目标(例如用于数据库驱动),可在“自定义序列化器”章节中学习。 + +请注意,虽然序列化器也会对数据的兼容性进行校验,但这些校验与[验证](validation.md)中的验证不同。只有 `cast` 函数在成功反序列化后还会调用[验证](validation.md)章节中的完整验证流程,并在数据无效时抛出错误。 + +或者,也可以使用 `validatedDeserialize` 在反序列化后进行验证。另一个选择是对来自 `deserialize` 函数的反序列化数据手动调用 `validate` 或 `validates` 函数,详见[验证](validation.md)。 + +序列化与验证中的所有函数在出错时都会抛出来自 `@deepkit/type` 的 `ValidationError`。 + +## Cast + +`cast` 函数的第一个类型参数是一个 TypeScript 类型,第二个参数是要转换的数据。数据会被转换为给定类型,若成功则返回该数据。如果数据与给定类型不兼容且无法自动转换,则会抛出 `ValidationError`。 + +```typescript +import { cast } from '@deepkit/type'; + +cast<string>(123); //'123' +cast<number>('123'); //123 +cast<number>('asdasd'); // 抛出 ValidationError + +cast<string | number>(123); //123 +``` + +```typescript +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = cast<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); +``` + +`deserialize` 函数与 `cast` 类似,但当数据与给定类型不兼容时不会抛出错误。取而代之的是,数据会尽可能被转换并返回结果。如果数据与给定类型不兼容,则按原样返回该数据。 + +## 序列化 + +```typescript +import { serialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const model = new MyModel('Peter'); + +const jsonObject = serialize<MyModel>(model); +//{ +// id: 0, +// created: '2021-06-10T15:07:24.292Z', +// name: 'Peter' +//} +const json = JSON.stringify(jsonObject); +``` + +`serialize` 函数默认使用 JSON 序列化器将传入数据转换为 JSON 对象,即:String、Number、Boolean、Object 或 Array。其结果随后可以安全地使用 `JSON.stringify` 转换为 JSON。 + +## 反序列化 + +`deserialize` 函数默认使用 JSON 序列化器将传入数据转换为相应的指定类型。JSON 序列化器期望的是一个 JSON 对象,即:string、number、boolean、object 或 array。这通常来自一次 `JSON.parse` 调用。 + +```typescript +import { deserialize } from '@deepkit/type'; + +class MyModel { + id: number = 0; + created: Date = new Date; + + constructor(public name: string) { + } +} + +const myModel = deserialize<MyModel>({ + id: 5, + created: 'Sat Oct 13 2018 14:17:35 GMT+0200', + name: 'Peter', +}); + +//来自 JSON +const json = '{"id": 5, "created": "Sat Oct 13 2018 14:17:35 GMT+0200", "name": "Peter"}'; +const myModel = deserialize<MyModel>(JSON.parse(json)); +``` + +如果已经传入了正确的数据类型(例如 `created` 为 Date 对象),则会直接按原样使用。 + +不仅可以指定类,任何 TypeScript 类型都可以作为第一个类型参数。因此即使是原始类型或非常复杂的类型也都可以传入: + +```typescript +deserialize<Date>('Sat Oct 13 2018 14:17:35 GMT+0200'); +deserialize<string | number>(23); +``` + +<a name="loosely-convertion"></a> +### 宽松类型转换 + +在反序列化过程中实现了宽松类型转换。这意味着 String 与 Number 可用于 String 类型,或者 Number 可用于 String 类型时会被自动接受并转换。这在通过 URL 接收数据并传递给反序列化器时非常有用。由于 URL 始终是字符串,Deepkit Type 仍会尝试解析 Number 和 Boolean 的类型。 + +```typescript +deserialize<boolean>('false')); //false +deserialize<boolean>('0')); //false +deserialize<boolean>('1')); //true + +deserialize<number>('1')); //1 + +deserialize<string>(1)); //'1' +``` + +JSON 序列化器内置了如下宽松类型转换: + +* *number|bigint*:Number 或 Bigint 接受 String、Number 和 BigInt。若需要转换,将使用 `parseFloat` 或 `BigInt(x)`。 +* *boolean*:Boolean 接受 Number 和 String。0、'0'、'false' 会被解释为 `false`。1、'1'、'true' 会被解释为 `true`。 +* *string*:String 接受 Number、String、Boolean 等多种类型。所有非字符串值都会通过 `String(x)` 自动转换。 + +也可以关闭宽松转换: + +```typescript +const result = deserialize(data, {loosely: false}); +``` + +在数据无效的情况下,将不会尝试进行转换,而是直接抛出错误信息。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/types.md b/website/src/translations/zh/documentation/runtime-types/types.md new file mode 100644 index 000000000..32c916293 --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/types.md @@ -0,0 +1,493 @@ +# 类型注解 + +类型注解是普通的 TypeScript 类型,它们包含可在运行时读取的元信息,并可改变各类函数的行为。Deepkit 已经提供了一些类型注解,覆盖了许多使用场景。例如,可以将类属性标记为主键、引用或索引。数据库库可以在运行时使用这些信息来创建正确的 SQL 查询,而无需事先进行代码生成。 + +验证器约束(如 `MaxLength`、`Maximum` 或 `Positive`)也可以添加到任意类型上。还可以告诉序列化器如何序列化或反序列化特定值。此外,还可以创建完全自定义的类型注解并在运行时读取它们,以便在运行时以非常个性化的方式使用类型系统。 + +Deepkit 自带一整套类型注解,全部都可以直接从 `@deepkit/type` 使用。它们的设计目标是不来自多个库,从而避免将代码直接绑定到某个特定库,如 Deepkit RPC 或 Deepkit Database。这样即使使用了例如数据库类型注解,也更易于在前端复用类型。 + +下面是现有类型注解的列表。`@deepkit/type` 和 `@deepkit/bson` 的验证器和序列化器以及 `@deepkit/orm` 的 Deepkit Database 会以不同方式使用这些信息。参见相应章节以了解更多内容。 + +## 整数/浮点数 + +整数和浮点数以 `number` 作为基础定义,并有多个子变体: + +| 类型 | 描述 | +|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| integer | 任意大小的整数。 | +| int8 | 介于 -128 和 127 之间的整数。 | +| uint8 | 介于 0 和 255 之间的整数。 | +| int16 | 介于 -32768 和 32767 之间的整数。 | +| uint16 | 介于 0 和 65535 之间的整数。 | +| int32 | 介于 -2147483648 和 2147483647 之间的整数。 | +| uint32 | 介于 0 和 4294967295 之间的整数。 | +| float | 与 number 相同,但在数据库上下文中可能具有不同含义。 | +| float32 | 介于 -3.40282347e+38 和 3.40282347e+38 的浮点数。请注意,由于精度问题,JavaScript 无法正确检查该范围,但这些信息对数据库或二进制序列化器可能很有用。 | +| float64 | 与 number 相同,但在数据库上下文中可能具有不同含义。 | + +```typescript +import { integer } from '@deepkit/type'; + +interface User { + id: integer; +} +``` + +这里用户的 `id` 在运行时是一个 number,但在验证和序列化中会被解释为整数。 +这意味着,例如,在验证中不允许使用浮点数,并且序列化器会自动将浮点数转换为整数。 + +```typescript +import { is, integer } from '@deepkit/type'; + +is<integer>(12); //true +is<integer>(12.5); //false +``` + +子类型可以以相同方式使用,当只允许特定范围的数字时非常有用。 + +```typescript +import { is, int8 } from '@deepkit/type'; + +is<int8>(-5); //true +is<int8>(5); //true +is<int8>(-200); //false +is<int8>(2500); //false +``` + +```typescript +import { is, float, float32, float64 } from '@deepkit/type'; +is<float>(12.5); //true +is<float32>(12.5); //true +is<float64>(12.5); //true +```` + +## UUID + +UUID v4 通常在数据库中以二进制存储,在 JSON 中以字符串存储。 + +```typescript +import { is, UUID } from '@deepkit/type'; + +is<UUID>('f897399a-9f23-49ac-827d-c16f8e4810a0'); //true +is<UUID>('asd'); //false +``` + +## MongoID + +将该字段标记为 MongoDB 的 ObjectId。在类型上解析为字符串。在 MongoDB 中以二进制存储。 + +```typescript +import { MongoId, serialize, is } from '@deepkit/type'; + +serialize<MongoId>('507f1f77bcf86cd799439011'); //507f1f77bcf86cd799439011 +is<MongoId>('507f1f77bcf86cd799439011'); //true +is<MongoId>('507f1f77bcf86cd799439011'); //false + +class User { + id: MongoId = ''; //在用户插入后将由 Deepkit ORM 自动设置 +} +``` + +## Bigint + +默认情况下,普通的 bigint 类型在 JSON 中序列化为 number(在 BSON 中为 long)。然而这在可保存的范围上有局限性,因为 JavaScript 中的 bigint 理论上大小无限,而 JavaScript 的 number 和 BSON 的 long 是有限的。为绕过此限制,提供了 `BinaryBigInt` 和 `SignedBinaryBigInt` 类型。 + +`BinaryBigInt` 与 bigint 相同,但在数据库中序列化为无符号二进制(大小不受限,而非大多数数据库中的 8 字节),并在 JSON 中序列化为字符串。负值将被转换为正数(`abs(x)`)。 + +```typescript +import { BinaryBigInt } from '@deepkit/type'; + +interface User { + id: BinaryBigInt; +} + +const user: User = { id: 24n }; + +serialize<User>({ id: 24n }); //{id: '24'} + +serialize<BinaryBigInt>(24); //'24' +serialize<BinaryBigInt>(-24); //'0' +``` + +Deepkit ORM 将 BinaryBigInt 作为二进制字段存储。 + +`SignedBinaryBigInt` 与 `BinaryBigInt` 相同,但也能存储负数。Deepkit ORM 将 `SignedBinaryBigInt` 作为二进制存储。该二进制有一个额外的前导符号字节,并以无符号整数表示:255 表示负数,0 表示零,1 表示正数。 + +```typescript +import { SignedBinaryBigInt } from '@deepkit/type'; + +interface User { + id: SignedBinaryBigInt; +} +``` + +## MapName + +用于在序列化时更改属性的名称。 + +```typescript +import { serialize, deserialize, MapName } from '@deepkit/type'; + +interface User { + firstName: string & MapName<'first_name'>; +} + +serialize<User>({ firstName: 'Peter' }) // {first_name: 'Peter'} +deserialize<User>({ first_name: 'Peter' }) // {firstName: 'Peter'} +``` + +## Group + +可以将属性分组。在序列化时,例如可以排除某个分组。更多信息参见“序列化”章节。 + +```typescript +import { serialize } from '@deepkit/type'; + +interface Model { + username: string; + password: string & Group<'secret'> +} + +serialize<Model>( + { username: 'Peter', password: 'nope' }, + { groupsExclude: ['secret'] } +); //{username: 'Peter'} +``` + +## Data + +每个属性都可以添加可通过反射 API 读取的额外元数据。更多信息参见[运行时类型反射](runtime-types.md#runtime-types-reflection)。 + +```typescript +import { ReflectionClass } from '@deepkit/type'; + +interface Model { + username: string; + title: string & Data<'key', 'value'> +} + +const reflection = ReflectionClass.from<Model>(); +reflection.getProperty('title').getData()['key']; //value; +``` + +## Excluded + +可将某个属性在特定目标的序列化过程中排除。 + +```typescript +import { serialize, deserialize, Excluded } from '@deepkit/type'; + +interface Auth { + title: string; + password: string & Excluded<'json'> +} + +const item = deserialize<Auth>({ title: 'Peter', password: 'secret' }); + +item.password; //undefined,因为 deserialize 的默认序列化器名为 `json` + +item.password = 'secret'; + +const json = serialize<Auth>(item); +json.password; //依然为 undefined,因为 serialize 的序列化器也叫 `json` +``` + +## Embedded + +将字段标记为嵌入类型。 + +```typescript +import { PrimaryKey, Embedded, serialize, deserialize } from '@deepkit/type'; + +interface Address { + street: string; + postalCode: string; + city: string; + country: string; +} + +interface User { + id: number & PrimaryKey; + address: Embedded<Address>; +} + +const user: User +{ + id: 12, + address +: + { + street: 'abc', postalCode + : + '1234', city + : + 'Hamburg', country + : + 'Germany' + } +} +; + +serialize<User>(user); +{ + id: 12, + address_street +: + 'abc', + address_postalCode +: + '1234', + address_city +: + 'Hamburg', + address_country +: + 'Germany' +} + +//对于反序列化,你需要提供嵌入后的结构 +deserialize<User>({ + id: 12, + address_street: 'abc', + //... +}); +``` + +可以更改前缀(默认是属性名)。 + +```typescript +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: 'addr_' }>; +} + +serialize<User>(user); +{ + id: 12, + addr_street +: + 'abc', + addr_postalCode +: + '1234', +} + +//或者完全移除前缀 +interface User { + id: number & PrimaryKey; + address: Embedded<Address, { prefix: '' }>; +} + +serialize<User>(user); +{ + id: 12, + street +: + 'abc', + postalCode +: + '1234', +} +``` + +## Entity + +为接口添加实体信息。仅用于数据库上下文。 + +```typescript +import { Entity, PrimaryKey } from '@deepkit/type'; + +interface User extends Entity<{ name: 'user', collection: 'users'> { + id: number & PrimaryKey; + username: string; +} +``` + +## PrimaryKey + +将字段标记为主键。仅用于数据库上下文。 + +```typescript +import { PrimaryKey } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} +``` + +## AutoIncrement + +将字段标记为自增。仅用于数据库上下文。 +通常与 `PrimaryKey` 一起使用。 + +```typescript +import { AutoIncrement } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey & AutoIncrement; +} +``` + +## Reference + +将字段标记为引用(外键)。仅用于数据库上下文。 + +```typescript +import { Reference } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; +} +``` + +在此示例中,`User.group` 是一个拥有方引用,也就是 SQL 中的外键。这意味着 `User` 表有一列 `group` 引用 `Group` 表。`Group` 表是该引用的目标表。 + +## BackReference + +将字段标记为反向引用。仅用于数据库上下文。 + +```typescript + +interface User { + id: number & PrimaryKey; + group: number & Reference<Group>; +} + +interface Group { + id: number & PrimaryKey; + users: User[] & BackReference; +} +``` + +在此示例中,`Group.users` 是一个反向引用。这意味着 `User` 表有一列 `group` 引用 `Group` 表。 +一旦执行带有联接的数据库查询,`Group` 会有一个虚拟属性 `users`,它会自动填充所有 `group` id 与该 `Group` 的 id 相同的用户。属性 `users` 本身不会存储在数据库中。 + +## Index + +将字段标记为索引。仅用于数据库上下文。 + +```typescript +import { Index } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Index; +} +``` + +## Unique + +将字段标记为唯一。仅用于数据库上下文。 + +```typescript +import { Unique } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & Unique; +} +``` + +## DatabaseField + +使用 `DatabaseField` 可以定义数据库特定的选项,如真实的数据库列类型、默认值等。 + +```typescript +import { DatabaseField } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; + username: string & DatabaseField<{ type: 'varchar(255)' }>; +} +``` + +## 验证 + +待办 + +参见[验证约束类型](validation.md#validation-constraint-types)。 + +## InlineRuntimeType + +用于内联一个运行时类型。仅用于高级场景。 + +```typescript +import { InlineRuntimeType, ReflectionKind, Type } from '@deepkit/type'; + +const type: Type = { kind: ReflectionKind.string }; + +type Query = { + field: InlineRuntimeType<typeof type>; +} + +const resolved = typeOf<Query>(); // { field: string } +``` + +在 TypeScript 中,类型 `Query` 是 `{ field: any }`,但在运行时是 `{ field: string }`。 + +当你构建一个高度可定制的系统、接收运行时类型并在各种其他场景中复用它们时,这会很有用。 + +## ResetAnnotation + +重置某个属性的所有注解。仅用于高级场景。 + +```typescript +import { ResetAnnotation } from '@deepkit/type'; + +interface User { + id: number & PrimaryKey; +} + +interface UserCreationPayload { + id: User['id'] & ResetAnnotation<'primaryKey'>; +} +``` + +### 自定义类型注解 + +你可以定义自己的类型注解。 + +```typescript +type MyAnnotation = { __meta?: ['myAnnotation'] }; +``` + +按照约定,类型注解被定义为仅包含一个可选属性 `__meta` 的对象字面量,该属性的类型是元组。该元组的第一个条目是其唯一名称,后续条目则是任意选项。这样可以为类型注解附加更多可选配置。 + +```typescript +type AnnotationOption<T extends { title: string }> = { __meta?: ['myAnnotation', T] }; +``` + +类型注解通过交叉类型运算符 `&` 使用。可以在一个类型上使用任意数量的类型注解。 + +```typescript +type Username = string & MyAnnotation; +type Title = string & MyAnnotation & AnnotationOption<{ title: 'Hello' }>; +``` + +可以通过 `typeOf<T>()` 和 `typeAnnotation` 的类型对象读取类型注解: + +```typescript +import { typeOf, typeAnnotation } from '@deepkit/type'; + +const type = typeOf<Username>(); +const annotation = typeAnnotation.getForName(type, 'myAnnotation'); //[] +``` + +`annotation` 的结果要么是一个包含选项的数组(如果使用了类型注解 `myAnnotation`),要么是 `undefined`(如果未使用)。如果类型注解具有如 `AnnotationOption` 所示的额外选项,则传入的值可以在数组中找到。 +已有的类型注解如 `MapName`、`Group`、`Data` 等有各自的注解对象: + +```typescript +import { typeOf, Group, groupAnnotation } from '@deepkit/type'; + +type Username = string & Group<'a'> & Group<'b'>; + +const type = typeOf<Username>(); +groupAnnotation.getAnnotations(type); //['a', 'b'] +``` + +参见[运行时类型反射](./reflection.md)以了解更多。 \ No newline at end of file diff --git a/website/src/translations/zh/documentation/runtime-types/validation.md b/website/src/translations/zh/documentation/runtime-types/validation.md new file mode 100644 index 000000000..c9f89dc71 --- /dev/null +++ b/website/src/translations/zh/documentation/runtime-types/validation.md @@ -0,0 +1,539 @@ +# 验证 + +验证是系统性地核验数据准确性和完整性的过程。这不仅包括检查数据类型是否与期望类型一致,还包括是否满足任何额外的预定义约束。 + +当处理来自不确定或不受信任来源的数据时,验证尤为重要。“不确定”的来源指的是其数据的类型或内容不可预测,运行时可能取任意值。典型例子包括用户输入、来自 HTTP 请求的数据(如查询参数或请求体)、CLI 参数,或程序读取的文件。此类数据天然存在风险,因为错误的类型或数值可能导致程序失败,甚至引入安全漏洞。 + +例如,如果变量预期存储一个数字,那么验证其确实为数值至关重要。类型不匹配可能导致意外崩溃或安全漏洞。 + +在设计 HTTP 路由控制器时,必须优先验证所有用户输入,无论是通过查询参数、请求体,还是其他方式。特别是在使用 TypeScript 的环境中,务必避免类型断言(type cast)。这些断言可能具有误导性,并引入根本性的安全风险。 + +```typescript +app.post('/user', function(request) { + const limit = request.body.limit as number; +}); +``` + +编码中常见的错误是依赖在运行时并不提供安全性的类型断言。例如,如果你将变量断言为 number,但用户却输入了字符串,程序就会被误导,按“该字符串是数字”来运行。这类疏忽可能导致系统崩溃或引发严重的安全威胁。为降低这些风险,开发者可以利用验证器(validator)和类型保护(type guard)。此外,序列化器(serializer)也可用于转换变量,如将 limit 转为数字。更多见“序列化”章节。 + +验证不是可选项;它是健壮软件设计的组成部分。宁可验证过度,也不要因验证不足而后悔。Deepkit 深知其重要性,提供了丰富的验证工具,而且其高性能设计确保对执行时间的影响最小。作为指导原则,请进行全面验证来保护你的应用,即使有时看起来有点“啰嗦”。 + +Deepkit 的许多组件,包括 HTTP 路由器、RPC 抽象,甚至数据库抽象,都内置了验证系统。这些机制会自动触发,通常无需手动干预。 + +要全面了解自动验证何时以及如何发生,请参阅特定章节([CLI](../cli.md)、[HTTP](../http.md)、[RPC](../rpc.md)、[ORM](../orm.md))。 +熟悉必要的约束与数据类型。合理定义参数可以释放 Deepkit 的自动化验证潜力,减少手工工作,确保代码更简洁、更安全。 + +## 用法 + +验证器的基本功能是按类型检查值。例如,某个值是否为字符串。这里不关心字符串的内容,仅关心其类型。TypeScript 中有许多类型:string、number、boolean、bigint、对象、类、interface、泛型、映射类型等。得益于 TypeScript 强大的类型系统,可用的类型种类非常丰富。 + +在 JavaScript 中,原始类型可以通过 `typeof` 运算符解析。对于更复杂的类型,如 interface、映射类型或泛型的 set/map,这就不再容易,像 `@deepkit/type` 这样的验证库就变得必不可少。Deepkit 是唯一能够直接验证所有 TypeScript 类型而无需任何变通方法的解决方案。 + +在 Deepkit 中,可以使用 `validate`、`is` 或 `assert` 函数进行类型验证。 +`is` 是所谓的类型保护(type guard),`assert` 是类型断言(type assertion)。两者会在下一节解释。 +`validate` 函数返回一个包含发现错误的数组,成功时返回空数组。该数组中的每一项描述了确切的错误代码、错误信息,以及在验证更复杂类型(如对象或数组)时的路径。 + +这三个函数的使用方式大致相同。将类型作为第一个类型参数指定或引用,并将数据作为第一个函数参数传入。 + +```typescript +import { validate, is, assert } from '@deepkit/type'; + +const errors = validate<string>('abc'); //[] +const errors = validate<string>(123); //[{code: 'type', message: '不是字符串'}] + +if (is<string>(value)) { + // value 现在被保证为字符串 +} + +function doSomething(value: any) { + assert<string>(value); //在数据无效时抛出 + + // value 现在被保证为字符串 +} +``` + +如果你使用更复杂的类型,如类或接口,返回的数组也可能包含多条目。 + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>(undefined); //[{code: 'type', message: '不是对象'}] + +validate<User>({}); +//[ +// {path: 'id', code: 'type', message: '不是数字'}], +// {path: 'username', code: 'type', message: '不是字符串'}], +//] +``` + +验证器也支持深度递归类型。路径将用点号分隔。 + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; + supervisor?: User; +} + +validate<User>({id: 1, username: 'Joe'}); //[] + +validate<User>({id: 1, username: 'Joe', supervisor: {}}); +//[ +// {path: 'supervisor.id', code: 'type', message: '不是数字'}], +// {path: 'supervisor.username', code: 'type', message: '不是字符串'}], +//] +``` + +充分利用 TypeScript 带来的优势。例如,更复杂的类型如 `User` 可以在多个地方复用,而无需一次次重复声明。比如如果需要在不包含 `id` 的情况下验证 `User`,可以使用 TypeScript 的工具类型快速高效地创建派生子类型。这非常符合 DRY(不要重复自己)的精神。 + +```typescript +type UserWithoutId = Omit<User, 'id'>; + +validate<UserWithoutId>({username: 'Joe'}); //有效! +``` + +Deepkit 是唯一能够在运行时以这种方式访问 TypeScript 类型的主流框架。如果想在前端和后端中共享类型,可以将类型提取到单独文件,从而在任意位置导入。利用这一点有助于保持代码高效且整洁。 + +## 类型断言不安全 + +TypeScript 中的类型断言(与类型保护相对)并非运行时构造,它只在类型系统中生效。用它给未知数据指定类型并不安全。 + +```typescript +const data: any = ...; + +const username = data.username as string; + +if (username.startsWith('@')) { //可能会崩溃 +} +``` + +`as string` 这段代码并不安全。变量 `data` 可能是任何值,例如 `{username: 123}`,甚至 `{}`,导致 `username` 并不是字符串,而是完全不同的东西,因此 `username.startsWith('@')` 会报错,在基础情况下导致程序崩溃,在最糟糕的情况下造成安全漏洞。 +要在运行时保证 `data` 拥有类型为字符串的 `username` 属性,必须使用类型保护。 + +类型保护是向 TypeScript 提示传入数据在运行时被保证为何种类型的函数。借助这些信息,TypeScript 会在代码执行过程中“收窄”类型。例如,可以将 `any` 安全地缩窄为字符串或其他类型。因此,如果存在类型未知的数据(`any` 或 `unknown`),类型保护可以基于数据本身更精确地缩窄它。然而,类型保护的安全性取决于其实现。如果实现有误,后果可能非常严重,因为基本假设会突然变得不成立。 + +<a name="type-guard"></a> + +## 类型保护(Type-Guard) + +对上文使用的 `User` 类型,其最简单形式的类型保护如下。注意,上文提到的关于 NaN 的特殊情况未在此体现,因此这个类型保护并不完全正确。 + +```typescript +function isUser(data: any): data is User { + return 'object' === typeof data + && 'number' === typeof data.id + && 'string' === typeof data.username; +} + +isUser({}); //false + +isUser({id: 1, username: 'Joe'}); //true +``` + +类型保护总是返回布尔值,通常直接用于 if 判断。 + +```typescript +const data: any = await fetch('/user/1'); + +if (isUser(data)) { + data.id; //可以安全访问,且为数字 +} +``` + +为每个类型手写一个类型保护函数,尤其是针对更复杂的类型,并在类型变化时反复修改,既繁琐又易出错且低效。因此,Deepkit 提供了 `is` 函数,它能为任意 TypeScript 类型自动提供类型保护。同时也会自动考虑诸如 NaN 问题等特殊情况。`is` 的功能与 `validate` 相同,但它返回布尔值而不是错误数组。 + +```typescript +import { is } from '@deepkit/type'; + +is<string>('abc'); //true +is<string>(123); //false + + +const data: any = await fetch('/user/1'); + +if (is<User>(data)) { + //data 现在被保证为 User 类型 +} +``` + +一种常见的模式是在验证失败时直接返回错误,以阻止后续代码执行。这可在多处使用,而无需改变整体代码流程。 + +```typescript +function addUser(data: any): void { + if (!is<User>(data)) throw new TypeError('No user given'); + + //data 现在被保证为 User 类型 +} +``` + +或者,可以使用 TypeScript 的类型断言。`assert` 函数会在给定数据未能正确通过类型验证时自动抛出错误。该函数的特殊签名(区别于普通函数)帮助 TypeScript 自动收窄传入变量。 + +```typescript +import { assert } from '@deepkit/type'; + +function addUser(data: any): void { + assert<User>(data); //数据无效时抛出 + + //data 现在被保证为 User 类型 +} +``` + +在这里,同样要发挥 TypeScript 的优势。可以通过各种 TypeScript 功能复用或定制类型。 + +<a name="error-reporting"></a> + +## 错误报告 + +`is`、`assert` 和 `validates` 函数的结果是布尔值。若要获取验证失败规则的详细信息,可使用 `validate` 函数。若全部验证成功,它返回空数组;出错时,数组包含一条或多条结构如下的条目: + +```typescript +interface ValidationErrorItem { + /** + * 指向属性的路径。对于深层路径,用点号分隔。 + */ + path: string; + /** + * 小写的错误代码,可用于标识该错误并进行翻译。 + */ + code: string, + /** + * 错误的自由文本描述。 + */ + message: string, +} +``` + +该函数将任意 TypeScript 类型作为第一个类型参数,待验证的数据作为第一个实参。 + +```typescript +import { validate } from '@deepkit/type'; + +validate<string>('Hello'); //[] +validate<string>(123); //[{code: 'type', message: '不是字符串', path: ''}] + +validate<number>(123); //[] +validate<number>('Hello'); //[{code: 'type', message: '不是数字', path: ''}] +``` + +也可用于复杂类型,如 interface、class 或泛型。 + +```typescript +import { validate } from '@deepkit/type'; + +interface User { + id: number; + username: string; +} + +validate<User>(undefined); //[{code: 'type', message: '不是对象', path: ''}] +validate<User>({}); //[{code: 'type', message: '不是数字', path: 'id'}] +validate<User>({id: 1}); //[{code: 'type', message: '不是字符串', path: 'username'}] +validate<User>({id: 1, username: 'Peter'}); //[] +``` + +<a name="constraints"></a> + +## 约束 + +除了类型检查,还可以为类型添加其他任意约束。这些附加的内容约束会在类型本身验证通过后自动进行。这适用于所有验证函数,如 `validate`、`is` 和 `assert`。 +一个约束例如可以是字符串必须有一定的最小/最大长度。这些约束通过[类型注解](./types.md)添加到实际类型上。可用的注解种类很多。如有更复杂需求,也可以按需定义并使用自定义注解。 + +```typescript +import { MinLength } from '@deepkit/type'; + +type Username = string & MinLength<3>; +``` + +使用 `&` 可以将任意数量的类型注解添加到实际类型上。结果类型(此处为 `username`)随后可用于所有验证函数以及其他类型中。 + +```typescript +import { is } from '@deepkit/type'; + +is<Username>('ab'); //false,因为最小长度为 3 +is<Username>('Joe'); //true + +interface User { + id: number; + username: Username; +} + +is<User>({id: 1, username: 'ab'}); //false,因为最小长度为 3 +is<User>({id: 1, username: 'Joe'}); //true +``` + +`validate` 函数会给出来自约束的有用错误信息。 + +```typescript +import { validate } from '@deepkit/type'; + +const errors = validate<Username>('xb'); +//[{ code: 'minLength', message: `最小长度为 3` }] +``` + +这些信息可以很方便地在表单中自动呈现,并可通过 `code` 进行翻译。借助对象和数组的路径信息,表单中的字段可以筛选并显示相应错误。 + +```typescript +validate<User>({id: 1, username: 'ab'}); +//{ path: 'username', code: 'minLength', message: `最小长度为 3` } +``` + +一个常见且有用的用例是使用正则表达式约束来定义 Email。一旦定义了该类型,就可以在任何地方复用。 + +```typescript +export const emailRegexp = /^\S+@\S+$/; +type Email = string & Pattern<typeof emailRegexp> + +is<Email>('abc'); //false +is<Email>('joe@example.com'); //true +``` + +可以添加任意数量的约束。 + +```typescript +type ID = number & Positive & Maximum<1000>; + +is<ID>(-1); //false +is<ID>(123); //true +is<ID>(1001); //true +``` + +### 约束类型 + +#### Validate<typeof myValidator> + +使用自定义验证函数进行验证。更多信息见下一节“自定义验证器”。 + +```typescript +import { ValidatorError, Validate } from '@deepkit/type'; + +function startsWith(v: string) { + return (value: any) => { + const valid = 'string' === typeof value && value.startsWith(v); + return valid ? undefined : new ValidatorError('startsWith', `Does not start with ${v}`); + }; +} + +type T = string & Validate<typeof startsWith, 'abc'>; +``` + +#### Pattern<typeof myRegexp> + +定义一个正则表达式作为验证模式。通常用于 Email 验证或更复杂的内容验证。 + +```typescript +import { Pattern } from '@deepkit/type'; + +const myRegExp = /[a-zA-Z]+/; +type T = string & Pattern<typeof myRegExp> +``` + +#### Alpha + +仅包含字母字符(a-Z)的验证。 + +```typescript +import { Alpha } from '@deepkit/type'; + +type T = string & Alpha; +``` + +#### Alphanumeric + +字母与数字字符的验证。 + +```typescript +import { Alphanumeric } from '@deepkit/type'; + +type T = string & Alphanumeric; +``` + +#### Ascii + +ASCII 字符的验证。 + +```typescript +import { Ascii } from '@deepkit/type'; + +type T = string & Ascii; +``` + +#### Decimal<number, number> + +验证字符串表示一个十进制数,如 0.1、.3、1.1、1.00003、4.0 等。 + +```typescript +import { Decimal } from '@deepkit/type'; + +type T = string & Decimal<1, 2>; +``` + +#### MultipleOf<number> + +验证数字是给定数字的倍数。 + +```typescript +import { MultipleOf } from '@deepkit/type'; + +type T = number & MultipleOf<3>; +``` + +#### MinLength<number>, MaxLength<number>, MinMax<number, number> + +为数组或字符串验证最小/最大长度。 + +```typescript +import { MinLength, MaxLength, MinMax } from '@deepkit/type'; + +type T = any[] & MinLength<1>; + +type T = string & MinLength<3> & MaxLength<16>; + +type T = string & MinMax<3, 16>; +``` + +#### Includes<'any'> Excludes<'any'> + +验证数组项或子字符串被包含/排除。 + +```typescript +import { Includes, Excludes } from '@deepkit/type'; + +type T = any[] & Includes<'abc'>; +type T = string & Excludes<' '>; +``` + +#### Minimum<number>, Maximum<number> + +验证数值不小于/不大于给定数字。对应 `>=` 和 `<=`。 + +```typescript +import { Minimum, Maximum, MinMax } from '@deepkit/type'; + +type T = number & Minimum<10>; +type T = number & Minimum<10> & Maximum<1000>; + +type T = number & MinMax<10, 1000>; +``` + +#### ExclusiveMinimum<number>, ExclusiveMaximum<number> + +与 Minimum/Maximum 类似,但不包含边界值本身。对应 `>` 和 `<`。 + +```typescript +import { ExclusiveMinimum, ExclusiveMaximum } from '@deepkit/type'; + +type T = number & ExclusiveMinimum<10>; +type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>; +``` + +#### Positive, Negative, PositiveNoZero, NegativeNoZero + +验证数值为正或为负。 + +```typescript +import { Positive, Negative } from '@deepkit/type'; + +type T = number & Positive; +type T = number & Negative; +``` + +#### BeforeNow, AfterNow + +将日期值与当前时间(new Date)比较的验证。 + +```typescript +import { BeforeNow, AfterNow } from '@deepkit/type'; + +type T = Date & BeforeNow; +type T = Date & AfterNow; +``` + +#### Email + +通过 `/^\S+@\S+$/` 进行简单的邮箱正则验证。它自动为 `string`,因此无需写成 `string & Email`。 + +```typescript +import { Email } from '@deepkit/type'; + +type T = Email; +``` + +#### integer + +确保数字是在正确范围内的整数。它自动为 `number`,因此无需写成 `number & integer`。 + +```typescript +import { integer, uint8, uint16, uint32, + int8, int16, int32 } from '@deepkit/type'; + +type T = integer; +type T = uint8; +type T = uint16; +type T = uint32; +type T = int8; +type T = int16; +type T = int32; +``` + +更多信息见“特殊类型:整数/浮点数”。 + +### 自定义验证器 + +如果内置验证器不够用,可以通过 `Validate` 装饰器创建并使用自定义验证函数。 + +```typescript +import { ValidatorError, Validate, Type, validates, validate } + from '@deepkit/type'; + +function titleValidation(value: string, type: Type) { + value = value.trim(); + if (value.length < 5) { + return new ValidatorError('tooShort', 'Value is too short'); + } +} + +interface Article { + id: number; + title: string & Validate<typeof titleValidation>; +} + +console.log(validates<Article>({id: 1})); //false +console.log(validates<Article>({id: 1, title: 'Peter'})); //true +console.log(validates<Article>({id: 1, title: ' Pe '})); //false +console.log(validate<Article>({id: 1, title: ' Pe '})); //[ValidationErrorItem] +``` + +请注意,你的自定义验证函数会在所有内置类型验证器调用之后执行。如果某个验证器失败,当前类型的后续验证器都会被跳过。每个类型最多只会产生一次失败。 + +#### 泛型验证器 + +在验证器函数中,可使用类型对象来获取使用该验证器的类型的更多信息。还可以定义任意验证器选项,作为参数传给验证类型,使验证器可配置。借助这些信息及其父级引用,可以创建功能强大的通用验证器。 + +```typescript +import { ValidatorError, Validate, Type, is, validate } + from '@deepkit/type'; + +function startsWith(value: any, type: Type, chars: string) { + const valid = 'string' === typeof value && value.startsWith(chars); + if (!valid) { + return new ValidatorError('startsWith', 'Does not start with ' + chars) + } +} + +type MyType = string & Validate<typeof startsWith, 'a'>; + +is<MyType>('aah'); //true +is<MyType>('nope'); //false + +const errors = validate<MyType>('nope'); +//[{ path: '', code: 'startsWith', message: `Does not start with a` }]); +``` \ No newline at end of file diff --git a/website/src/translations/zh/state.json b/website/src/translations/zh/state.json new file mode 100644 index 000000000..73e29dfb6 --- /dev/null +++ b/website/src/translations/zh/state.json @@ -0,0 +1,116 @@ +{ + "index.md": "29488f37f0c90cb66e32023aa3869f6081dab42590def9196514de8e51e977de", + "introduction.md": "7159b93c7cf42ed98bd0a86ed2b0100884e1eae755e56d8392e9a4dfbe306dd0", + "app.md": "f1f4383a395605c4cd1c36314b61afeebc6369e8c624080ba4f005bd064db99d", + "app/arguments.md": "009c2438d897ea0f25d569d9b3b9654e55d1a01ad3c04bf8b4c512a1b9789f59", + "app/dependency-injection.md": "b89fd5d14b2e0180a5c29f31e79ecc4112d328d9efe89f4e5a3ca8caa88129f5", + "app/modules.md": "9d0c92e9db99c4e96c00152a51f69efa49c974860e33937aec6144d10795dc27", + "app/services.md": "9df90a136ffed203714a4427137ba9bfda6901eb77c28dfe0d8c305f135c06c3", + "app/events.md": "45af019cd3a41e6e5c4987b6d7980d5103afb20a889aba7c9a8d04cae73b3dba", + "app/logger.md": "467d21c830908bc4a49c3a3d80dd6c9207c3403977a693788990b04d89f2c8d6", + "app/configuration.md": "5cd9710e8c7037e4af0a43f47095332fca70a9c0a1e267f249d0f89e1d0c995e", + "framework.md": "7d23eb8115535150c547adc60d8677c9e74f7aa6d0f9ba2aa2c34dd3c61c1f72", + "framework/database.md": "3082875ba15ff97becd94966634dc57a4e3a96e0e09db1bd1a901faeecfc3a9f", + "framework/testing.md": "f22048a69772b752d7174645e25708288f67721ac4f16774e7c3786d9143691f", + "framework/deployment.md": "9f43e70a78caf28a58a76e6b6f4b654dca8e83fef9a5dfacd0544c9a40251ee0", + "framework/public.md": "014cf4e0702d543c6aa086a1256e024d63a06fe761f4352e06ba953a3ae5d39f", + "runtime-types.md": "ddb2b9e67054ede0ca0937044b4876882326ea421d7a9919633ae8187782bff7", + "runtime-types/getting-started.md": "437cb0b8ba80b036e023719d8bd56ad22d5414420199b9fa7561afba1f68b65a", + "runtime-types/types.md": "f8b43c9534f850990aee6e57dfcd5bffdaf087d953b2edc3f95dea5b9cc53b6d", + "runtime-types/reflection.md": "5b96135eeb9281f65ca5c365d4697868a31ce2a85b141e0283070ac3bec8dae9", + "runtime-types/serialization.md": "9d1d7adac33dc116244b6edb7cf37c88c7d7657c3692d3e354e04c48b71d7561", + "runtime-types/validation.md": "9ba4b9f65b3f2c882e1df373411652df407b1ee9300faed54d6f4b62b4a0f986", + "runtime-types/extend.md": "2b88f476dac2c88ffcd05719177b9c5e43281b65cd3925698df14483f3de4792", + "runtime-types/custom-serializer.md": "b3da230cee1630ea1eb7d89f7b3e241726c3b956dffb6a8a3f94da0a2ea8ef12", + "runtime-types/external-types.md": "216d15bd8b956f8821bd1c3c86571550beabe9b769237514b232b4dd8af72714", + "runtime-types/bytecode.md": "b22322df3dcc093da4d776a1b68d3d11ae5e8c8053723f785ad2a25c6a2e8d2e", + "dependency-injection.md": "c1ddf1c3c6dc56783c7e282605f5b3b0ea50f81cd8d614ff77a6459e3893f3d4", + "dependency-injection/getting-started.md": "4a381a82a5b5aa208c44d08334b6b7ae8ee129878f1a212b08230371cee99556", + "dependency-injection/providers.md": "64ae199092c89687a393ec3b50722c746e1048fb9aceed2c5f4d61150c43c573", + "dependency-injection/injection.md": "399be234a76ea3575f53a1c1bf04ab7368b4d9711b3811f5be69ccdb275da80c", + "dependency-injection/configuration.md": "9f84fa97310995897afafdf2b6b5376858ad22722d25a19562b1f70051a41009", + "dependency-injection/scopes.md": "7886d8e715ef18a5b058a89fe099cf4f5ce2947495c7f1fb4a8fe1908ec0cf59", + "filesystem.md": "97fc7c9f028efb934b9ad864135420007cd1910c7b8a7b3b8bdc86b0b8e90cdf", + "filesystem/app.md": "f7aa47e42604191de2d549f1f8e5e9582394e55da0c8d470147783b62a6a383d", + "filesystem/local.md": "e62660fe771d5c22c1414a055bc96e77a92da04e780f8853be9a5d327a2077b2", + "filesystem/memory.md": "67e061e990066271c4e6d92e1552319a8f1d3e2b5b97b2a26a9dc2e88858e906", + "filesystem/database.md": "8d1132ef3880a90e48e27598514ce6f75eed6328658e7c29b3af9509b8735859", + "filesystem/aws-s3.md": "989c857dbe659e4c6a765442143945402ed49db36248958a123c3d11649b4148", + "filesystem/ftp.md": "3400d186b86e1abb2823a0d64a143720099b2d5ba9c9e59e78ce1cc24d74df65", + "filesystem/sftp.md": "f54ebc5db81bf8cc4e3e21e922168db5456ba9912abbcb456a17d7603ffee4db", + "filesystem/google-storage.md": "c20d8abfc8e10c26844bc8a154ca8eacdf202c812cdde45911e30086943429cb", + "broker.md": "0e97c1ea8ba83e929555018e47a2e7ea9a4d951a2eda3fdd3c68ca2e83dc9c99", + "broker/cache.md": "cfbc3dc50876e5951c848a60a82c5b83d34dc351cb325a64850e6910a774ba34", + "broker/message-bus.md": "8fec0f694e08bd4e894ddafa8d97ce4183b8713d1d9be93dcb1bde4df7ac700c", + "broker/message-queue.md": "cbc54b3d2ea7aba249948f06d8f622defe7cfcae9ef5e0fa48f97b876e8346fb", + "broker/atomic-locks.md": "90703416ba1fec9f5c14eee4972ee78b983d126544737f2801188962741d87c8", + "broker/key-value.md": "da10a448f38971d72963500f4cfc06ad8253d79808d877453f6e5734cdf30070", + "http.md": "2c20d0e955afe136ada302e197f176255aac7d941760c208f0dc75c2f3cf258a", + "http/getting-started.md": "59605a5fe0ca617bb70e3fd0014340b9b57a88e8e91fac9bdc3eaf0a8f8f11c1", + "http/input-output.md": "c225ccfafb21816bd804a96110e69301baa0dbb57304baed8a7595b771f46f77", + "http/views.md": "62ce86b45d849b7946608aeea23f88f1078d74f971703709c98f8ad476944893", + "http/dependency-injection.md": "65d42f23a838af794cc43e60aaa2ae6cd3d72e2d0fc275065e65c6c639a6e534", + "http/events.md": "aa47d884f82e724f1dcb5b33b10624bef1e9a04b84f97c4db5a8d3f8da6bd33b", + "http/middleware.md": "9485a78862a08ff1de9cfd52b89f095676e70e058ec732b8e5617d96e961372a", + "http/security.md": "cde7cb77eb3b2aa51456d20d29a06ea7ab205b40ef9ad586dbfd136cd498019c", + "rpc.md": "cf15217c23ae52a86ab7dcf628c668d5d06b9720ef4ddb3717f02994a3d507f7", + "rpc/getting-started.md": "b09000d9e2d9dfe6527b8a42cbec75ae2206284d4395b75e21a8e1c596430ba9", + "rpc/dependency-injection.md": "c8e5218600779f3c3c4fcdccbe7ab8add343f8a8c7f2847dc3b80da96d1604e9", + "rpc/security.md": "790bb3ff8ef236250d29d16b55e8b685be273f52accb3a368be71eed4a8e639d", + "rpc/errors.md": "7785519ce38ddd349353e71bcc62bb5ba74582f52c17378508f4c6e9051609a9", + "rpc/transport.md": "9561d6ccbfeaf511b6a914fd3d7e6f4b84411f70938ad639665f113e469e1991", + "orm.md": "e922d5a0e28aa3753e5332418f2802f1ae62596d17ae96f46d3deca59c4e7c0b", + "orm/getting-started.md": "56e4f565f748aaf043ce8710fc5a58402fb5889366d1478a267e26628ff4e0f6", + "orm/entity.md": "5fccac031df6723dc0e6bdc9ef7ecd4268631b3b14854661d8ce6dc918f9959c", + "orm/session.md": "88c774506bcca58e5c272f59b844846ebe4efe5ab2e3a367528764b0dc5dfdee", + "orm/query.md": "540a355c2c1666c2a4d9e7c8cc202994b732b17408508fd6944dde1f62a99646", + "orm/transactions.md": "b843d35bab46190b24a79f05950b6a301349bf539c01aae7b90716f5d0153b41", + "orm/inheritance.md": "943f367861fcaebffe70d16c4d795a739b16537bed0ca602d0343e0684ebc0ae", + "orm/relations.md": "9cfd7b3516d704e091207b0d78995b26df6d5b88b1c8181ee021ee0d7767795a", + "orm/events.md": "220d9066075c3a82e354dc8e6fcf82db649e440b8c3df0c0d2722ac5bec9dcfd", + "orm/migrations.md": "2ee65d0ccd58635e9cd115a3abf83b2488ddc925a7d5f5b25d8844bcedc107e6", + "orm/orm-browser.md": "ee8940fa282c2d42861ce203524277f4672c880a3e79cffdc978c55e795cec1b", + "orm/raw-access.md": "55c6e5a40386cff597293293536be5b47b082122432156b50bf06b386d3bb694", + "orm/seeding.md": "92bad8e262e1afdc818b39e6aa91c50a9016ad37da273defb7caf575821e6539", + "orm/composite-primary-key.md": "155cb42a4dcd9deffaa67aa30369d86c37826a28740977fe1905ef0c61bb9129", + "orm/plugin-soft-delete.md": "afa2c7f42833bddda5f7e116a3e7eabfc7183d112812e6c4e8c3a802aca5c3ff", + "package/angular-ssr.md": "e7e74dfdd8f957e82061c449938c2e1288031b976921e54e28b4f0d35b5d491d", + "package/api-console.md": "82d5322b37c517da883f567a18d4f64ecbcd61570300ac337e5e3c1473359880", + "package/app.md": "e60ef9f596cc6df26f490350a0001a912a715d463c762ed09ca6bf796e4cb4b3", + "package/bench.md": "f7a06c685a2f05aa397a2f271bec2a98c729c4273591e6996381b420a57116df", + "package/broker.md": "e46abbefd581c101d77fdf35151eddeeba32074d24f6e7b737841c3cefa2b76f", + "package/broker-redis.md": "67bbf67a50265ada9755bf093cc3d566b8ff21dd10a2cd1f3ed5f589826aac1a", + "package/bson.md": "dbbadeca42bacda8a7cb2e7cce55e9627463537cf16de4458dd029163575063e", + "package/bun.md": "a5627944c66b654c3b64e380fa58acebf0a22f4b045ae218880d882973f457eb", + "package/core.md": "fabeee88c765e0a0713cfafa7cc502bd4f04fa4c69467a8a9985d7d74b5b963e", + "package/core-rxjs.md": "42784cd0fa8c85d0473753e1cee7b769bf03d4d220f18add821bfa402624619f", + "package/devtool.md": "9fa98e55994ae65e50032d11c9c01f7fb52fbe0dec22cc9b2b6d5dbbe67af122", + "package/event.md": "4bcb6f33e7b788d4f2db94f8871ccded00c6523cd80bcf593d732293d207e148", + "package/filesystem.md": "0c2634e9f2f938d1bec04d201e449642f2573c1e0790c1fb783dccea91b4fa63", + "package/filesystem-aws-s3.md": "40cfd7a7cd0d0f495d9e76f693792859accd6e56a92d6defbff0458cd55ee73d", + "package/filesystem-database.md": "61848eaf206556fbc5d317eb7a1286288bc48329ad209b7dc4ce6daaa1ce374f", + "package/filesystem-ftp.md": "fdbd9659eb784a5f7cc8219a59c647b7646c7de939e424fe23ea9c6fe0306d3a", + "package/filesystem-google.md": "549623afd3bfa25d793758893360902751a21b0b6ae900e27448c8cb2bc8593c", + "package/filesystem-sftp.md": "b2966556da65453da7b98a6900667b2fb97bab48c986bbdae667c172fec44b41", + "package/framework.md": "027c9d986ec7e32a3b93a857f4cdb80367d17b54faf59c7d1620d8a21f7c4628", + "package/http.md": "159c8f0553a1cd8f71598ef7128a78525b7ad55999b8b2270acbdd05435352fd", + "package/injector.md": "f01def47d678021bb90a48e20d7e94ce18e865422affcf39314d66d04218b11e", + "package/logger.md": "0336bedc41fda399389fbb9085032091312973f33e3ec9bd4ed46aa931469100", + "package/mongo.md": "359d07c8c17320d28840fbdfa0c3eb219f5eedd65b14866ab3ac64d25d502c6f", + "package/mysql.md": "0bde461414a0669797ff3c9c1c1ab6954dbb1bd6d1fd11a4d916fa8cb41ea683", + "package/orm.md": "2e622c77f4477c7e69f330b68b3253e3d54cf9ab4c124935269a677c31753919", + "package/orm-browser.md": "534cb7f207f14d425a3ccd03e2ad038137d3732ea1fd51a67019b315e02e766c", + "package/postgres.md": "9296b10815117f3f96af3171c36a554b8fb5033b4328d956338ce69adcb18d04", + "package/rpc.md": "c455f9ab0212e27b208b29948c9979feb9fd05143248474f91d5dce0b6c1d236", + "package/rpc-tcp.md": "4924f5e05bec8a288008315edef65bcc1ee5d940cc87b0126b82a9f188b78079", + "package/run.md": "16f7e3360ffcabe09bda174e6c8ed64055a869c41d2355c4d55344fde25ee411", + "package/sql.md": "8b4046cc206ff85449595ed7debce715e487097a3e9446f1841b7ef213c932dd", + "package/sqlite.md": "64fc4711e3557b9453300d2a99f01d69ea640d728483984ff35ea13142a79efb", + "package/stopwatch.md": "f8dc8d14b28ee81a00ac9c30f9278ce70ece11837dd132d47dfd0e70ed7fa8af", + "package/template.md": "135fe797c2a4309e2257bd118218927ce1b1d1265fa2297f5f02c75b4b62c5dc", + "package/topsort.md": "a0c2258b8859c5c7d864758f27699835d2017a630d3855eb08df4d4e36024def", + "package/type.md": "7d28ae8aca6dbbd0439bd7c778f5871d3ba7305660edeefa8fb7aa5ae80bfb3d", + "package/type-compiler.md": "819bcb710c7cc610238a69b37eb6809d693ccce84f3dc239642591d0cfcf9426", + "package/vite.md": "df1a5256d86a3560f3f0384122779255b9afe038c0c0eca4f705a23c336b51e2", + "package/workflow.md": "ebe03caf6abcd0ea512dd560ccf54c9fb34798b66c87780e6c24ce295cf8e76d" +} \ No newline at end of file