{"id":443853,"date":"2025-01-02T15:03:09","date_gmt":"2025-01-02T15:03:09","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=443853"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2025-01-02T15:03:09","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=443853","title":{"rendered":"<span>\u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044f \u0434\u0430\u0442\u044b \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u00abNestJS\u00bb, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0432\u0432\u043e\u0434 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u044b \u0432 \u00abAngular\u00bb<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><strong>\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f:<\/strong> <a href=\"https:\/\/habr.com\/ru\/articles\/866858\/\" rel=\"noopener noreferrer nofollow\">\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u0444\u0443\u043b\u0441\u0442\u0435\u043a-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u043d\u0430 &#171;Angular&#187; \u0438 &#171;NestJS&#187;<\/a><\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0438 \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044f <code>workUntilDate<\/code> \u0441 \u0442\u0438\u043f\u043e\u043c <code>timestamp(6)<\/code> \u0432 \u0442\u0430\u0431\u043b\u0438\u0446\u0443 <code>Webhook<\/code> \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 <code>Webhook<\/code>.<\/p>\n<p>\u041d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430 (\u0432 <code>Angular<\/code>-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438) \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0431\u0443\u0434\u0435\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0443\u0434\u043e\u0431\u043d\u044b\u0439 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044c \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0432\u044b\u0431\u043e\u0440\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438.<\/p>\n<p>\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0441\u043c\u043e\u0433\u0443\u0442 \u0437\u0430\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0443 \u0438 \u0432\u0440\u0435\u043c\u044f \u0432 \u0441\u0432\u043e\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u0435, \u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a \u0431\u044d\u043a\u0435\u043d\u0434 (<code>NestJS<\/code>-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435) \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0432\u0432\u0435\u0434\u0451\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 <code>UTC+0<\/code>.<\/p>\n<p>\u041a\u0440\u043e\u043c\u0435 \u0442\u043e\u0433\u043e, \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044f \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0435 \u0434\u0430\u0442\u044b, \u0431\u0443\u0434\u0443\u0442 \u0430\u0434\u0430\u043f\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u043f\u043e\u0434 \u044f\u0437\u044b\u043a \u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u0437\u043e\u043d\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<h4>1. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a<\/h4>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u044b\u0435 \u043f\u0430\u043a\u0435\u0442\u044b:<\/p>\n<p><em>\u041a\u043e\u043c\u0430\u043d\u0434\u044b<\/em><\/p>\n<pre><code class=\"bash\">npm install --save @jsverse\/transloco-locale @jsverse\/transloco-messageformat --prefer-offline --no-audit --progress=false <\/code><\/pre>\n<h4>2. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438<\/h4>\n<p>\u041c\u043e\u0438 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u044b \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0445 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e.<\/p>\n<p>\u042d\u0442\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0432 \u0442\u0435\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445, \u043a\u043e\u0433\u0434\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0435\u0451 \u0437\u0430\u043d\u043e\u0432\u043e.<\/p>\n<p><em>\u041a\u043e\u043c\u0430\u043d\u0434\u044b<\/em><\/p>\n<pre><code class=\"bash\">npm run flyway:create:webhook --args=AddFieldWorkUntilDateToAuthUser <\/code><\/pre>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook\/src\/migrations\/V202412200905__AddFieldWorkUntilDateToAuthUser.sql<\/em><\/p>\n<pre><code class=\"sql\">DO $$ BEGIN     ALTER TABLE \"Webhook\"         ADD \"workUntilDate\" timestamp(6); EXCEPTION     WHEN duplicate_column THEN         NULL; END $$;   <\/code><\/pre>\n<h4>3. \u041f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 &#171;Prisma&#187;-\u0441\u0445\u0435\u043c<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0443\u044e \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044e, \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u0445\u0435\u043c\u044b <code>Prisma<\/code> \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043c <code>Prisma<\/code>-\u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440\u044b.<\/p>\n<p><em>\u041a\u043e\u043c\u0430\u043d\u0434\u044b<\/em><\/p>\n<pre><code class=\"bash\">npm run docker-compose:start-prod:server npm run db:create-and-fill npm run prisma:pull npm run generate <\/code><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u044d\u0442\u0438\u0445 \u0448\u0430\u0433\u043e\u0432, \u0432\u043e \u0432\u0441\u0435\u0445 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 <code>DTO<\/code> \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u043d\u043e\u0432\u043e\u0435 \u043f\u043e\u043b\u0435 <code>workUntilDate<\/code>.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f <code>DTO<\/code>-\u0444\u0430\u0439\u043b\u0430 <em>libs\/feature\/webhook\/src\/lib\/generated\/rest\/dto\/webhook.dto.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Prisma } from '..\/..\/..\/..\/..\/..\/..\/..\/node_modules\/@prisma\/webhook-client'; import { ApiProperty } from '@nestjs\/swagger';  export class WebhookDto {   \/\/ ...   \/\/ updates   @ApiProperty({     type: 'string',     format: 'date-time',     nullable: true,   })   workUntilDate!: Date | null; } <\/code><\/pre>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f <code>Prisma<\/code>-\u0441\u0445\u0435\u043c\u044b <em>libs\/feature\/webhook\/src\/prisma\/schema.prisma<\/em><\/p>\n<pre><code>generator client {   provider = \"prisma-client-js\"   engineType = \"binary\"   output   = \"..\/..\/..\/..\/..\/node_modules\/@prisma\/webhook-client\"   binaryTargets = [\"native\",\"linux-musl\",\"debian-openssl-1.1.x\",\"linux-musl-openssl-3.0.x\"] }  \/\/ ...  model Webhook {   id                                         String       @id(map: \"PK_WEBHOOK\") @default(dbgenerated(\"uuid_generate_v4()\")) @db.Uuid   \/\/ ...   workUntilDate                              DateTime?    @db.Timestamp(6) \/\/\/ &lt;-- updates } <\/code><\/pre>\n<h4>4. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 &#171;AsyncLocalStorage&#187; \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f<\/h4>\n<p>\u0420\u0430\u043d\u0435\u0435 \u043c\u044b \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u043b\u0438 <code>AuthTimezoneInterceptor<\/code> \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0434\u0430\u0442\u0430\u043c\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 <code>UTC-0<\/code> \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0439 \u0434\u0430\u0442\u044b \u0438\u0437 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u0434\u0430\u0442\u0443 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 <code>UTC-0<\/code>, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043e\u043d\u0430 \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 <code>AuthTimezonePipe<\/code>.<\/p>\n<p>\u041e\u0434\u043d\u0430\u043a\u043e \u0432 \u044d\u0442\u043e\u043c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435 \u0443 \u043d\u0430\u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u0430\u043d\u043d\u044b\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u0435\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u0437\u043e\u043d\u0443.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0440\u0435\u0448\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u043c\u044b \u043e\u0431\u0435\u0440\u043d\u0435\u043c \u043a\u0430\u0436\u0434\u044b\u0439 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432 <code>AsyncLocalStorage<\/code>, \u0447\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/core\/auth\/src\/lib\/interceptors\/auth-timezone.interceptor.ts<\/em><\/p>\n<pre><code class=\"typescript\">\/\/ ... import { AsyncLocalStorage } from 'node:async_hooks'; import { AuthAsyncLocalStorageData } from '..\/types\/auth-async-local-storage-data';  @Injectable() export class AuthTimezoneInterceptor implements NestInterceptor&lt;TData, TData&gt; {   constructor(     \/\/ ...     private readonly asyncLocalStorage: AsyncLocalStorage&lt;AuthAsyncLocalStorageData&gt;   ) {}    intercept(context: ExecutionContext, next: CallHandler) {     const req: AuthRequest = getRequestFromExecutionContext(context);     const userId = req.authUser?.externalUserId;      if (!this.authEnvironments.useInterceptors) {       return next.handle();     }      if (!userId) {       return next.handle();     }      const run = () =&gt; {       const result = next.handle();        if (isObservable(result)) {         return result.pipe(           concatMap(async (data) =&gt; {             const user = await this.authCacheService.getCachedUserByExternalUserId(userId);             return this.authTimezoneService.convertObject(data, user?.timezone);           })         );       }       if (result instanceof Promise &amp;&amp; typeof result?.then === 'function') {         return result.then(async (data) =&gt; {           if (isObservable(data)) {             return data.pipe(               concatMap(async (data) =&gt; {                 const user = await this.authCacheService.getCachedUserByExternalUserId(userId);                 return this.authTimezoneService.convertObject(data, user?.timezone);               })             );           } else {             const user = await this.authCacheService.getCachedUserByExternalUserId(userId);             \/\/ need for correct map types with base method of NestInterceptor             return this.authTimezoneService.convertObject(data, user?.timezone) as Observable&lt;TData&gt;;           }         });       }       \/\/ need for correct map types with base method of NestInterceptor       return this.authTimezoneService.convertObject(result, req.authUser?.timezone) as Observable&lt;TData&gt;;     };      if (!this.authEnvironments.usePipes) {       return run();     }      return this.asyncLocalStorage.run({ authTimezone: req.authUser?.timezone || 0 }, () =&gt; run());   } } <\/code><\/pre>\n<h4>5. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 &#171;Pipe&#187; \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430<\/h4>\n<p>\u041c\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c <code>Pipe<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0447\u0438\u0442\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u0437\u043e\u043d\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u0437 \u0432\u0441\u0435\u0445 \u043f\u043e\u043b\u0435\u0439 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0445 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441 \u0434\u0430\u0442\u0430\u043c\u0438.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u0437\u043e\u043d\u0430 \u0441\u0430\u043c\u043e\u0433\u043e \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 <code>UTC-0<\/code>, \u0442\u043e \u043e\u0442\u043d\u0438\u043c\u0430\u0435\u043c \u0440\u0430\u0437\u043d\u0438\u0446\u0443.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/core\/auth\/src\/lib\/pipes\/auth-timezone.pipe.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { SERVER_TIMEZONE_OFFSET } from '@nestjs-mod-fullstack\/common'; import { Injectable, PipeTransform } from '@nestjs\/common'; import { AsyncLocalStorage } from 'node:async_hooks'; import { AuthEnvironments } from '..\/auth.environments'; import { AuthTimezoneService } from '..\/services\/auth-timezone.service'; import { AuthAsyncLocalStorageData } from '..\/types\/auth-async-local-storage-data';  @Injectable() export class AuthTimezonePipe implements PipeTransform {   constructor(private readonly asyncLocalStorage: AsyncLocalStorage&lt;AuthAsyncLocalStorageData&gt;, private readonly authTimezoneService: AuthTimezoneService, private readonly authEnvironments: AuthEnvironments) {}    transform(value: unknown) {     if (!this.authEnvironments.usePipes) {       return value;     }     const result = this.authTimezoneService.convertObject(value, -1 * (this.asyncLocalStorage.getStore()?.authTimezone || 0) - SERVER_TIMEZONE_OFFSET);     return result;   } } <\/code><\/pre>\n<h4>6. \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0446\u0435\u043f\u0442\u043e\u0440\u0430 \u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u043c\u043e\u0434\u0443\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0446\u0435\u043f\u0442\u043e\u0440 \u0438 \u0441\u0435\u0440\u0432\u0438\u0441 \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u043c\u043e\u0434\u0443\u043b\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/core\/auth\/src\/lib\/auth.module.ts<\/em><\/p>\n<pre><code class=\"typescript\">\/\/ ... import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs\/core'; \/\/ ... import { AsyncLocalStorage } from 'node:async_hooks'; import { AuthTimezonePipe } from '.\/pipes\/auth-timezone.pipe';  export const { AuthModule } = createNestModule({   \/\/ ...   sharedProviders: [     {       provide: AsyncLocalStorage,       useValue: new AsyncLocalStorage(),     },     AuthTimezoneService,     AuthCacheService,   ],   providers: [     \/\/ ...     { provide: APP_PIPE, useClass: AuthTimezonePipe },     AuthAuthorizerService,     AuthAuthorizerBootstrapService,   ],   \/\/ ... }); <\/code><\/pre>\n<h4>7. \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u043f\u043e\u043b\u044f &#171;date-input&#187; \u0434\u043b\u044f &#171;Formly&#187;<\/h4>\n<p>\u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0435 <code>HTML<\/code>-\u043f\u043e\u043b\u0435 \u0432\u0432\u043e\u0434\u0430 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0432\u043e\u0434 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0442\u0438\u043f\u043e\u043c <code>Date<\/code>, \u0435\u0433\u043e \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0432\u0438\u0434 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445 <code>ng.ant.design<\/code>.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0435\u0434\u0438\u043d\u043e\u043e\u0431\u0440\u0430\u0437\u0438\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430, \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b <code>date-input<\/code> \u0434\u043b\u044f <code>Formly<\/code>.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/common-angular\/src\/lib\/formly\/date-input.component.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { AsyncPipe } from '@angular\/common'; import { ChangeDetectionStrategy, Component } from '@angular\/core'; import { ReactiveFormsModule } from '@angular\/forms'; import { TranslocoService } from '@jsverse\/transloco'; import { FieldType, FieldTypeConfig, FormlyModule } from '@ngx-formly\/core'; import { NzDatePickerModule } from 'ng-zorro-antd\/date-picker'; import { map, Observable } from 'rxjs'; import { DATE_INPUT_FORMATS } from '..\/constants\/date-input-formats'; import { ActiveLangService } from '..\/services\/active-lang.service';  @Component({   selector: 'date-input',   standalone: true,   changeDetection: ChangeDetectionStrategy.OnPush,   imports: [ReactiveFormsModule, FormlyModule, NzDatePickerModule, AsyncPipe],   template: ` &lt;nz-date-picker [formControl]=\"formControl\" [formlyAttributes]=\"field\" [nzShowTime]=\"true\" [nzFormat]=\"(format$ | async)!\"&gt;&lt;\/nz-date-picker&gt; `, }) export class DateInputComponent extends FieldType&lt;FieldTypeConfig&gt; {   format$: Observable&lt;string&gt;;    constructor(private readonly translocoService: TranslocoService, private readonly activeLangService: ActiveLangService) {     super();     this.format$ = translocoService.langChanges$.pipe(       map((lang) =&gt; {         const { locale } = this.activeLangService.normalizeLangKey(lang);         return DATE_INPUT_FORMATS[locale] ? DATE_INPUT_FORMATS[locale] : DATE_INPUT_FORMATS['en-US'];       })     );   } } <\/code><\/pre>\n<p>\u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044c \u0442\u0435\u043f\u0435\u0440\u044c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438, \u043e\u0434\u043d\u0430\u043a\u043e \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u0441\u0430\u043c\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0432\u0432\u043e\u0434\u0430 \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043d\u0435\u0438\u0437\u043c\u0435\u043d\u043d\u044b\u043c.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0440\u0435\u0448\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u043f\u0438\u0441\u043e\u043a \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043b\u043e\u043a\u0430\u043b\u0435\u0439 \u0438 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432 \u0432\u044b\u0432\u043e\u0434\u0430 \u0438 \u043d\u0430\u0441\u0442\u0430\u0440\u0438\u0432\u0430\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u044b\u0432\u043e\u0434\u0430 \u0434\u0430\u0442\u044b \u0432 <code>input<\/code>.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/common-angular\/src\/lib\/constants\/date-input-formats.ts<\/em><\/p>\n<pre><code class=\"typescript\">export const DATE_INPUT_FORMATS = {   'en-US': 'MM\/dd\/yyyy HH:mm:ss',   'en-GB': 'dd\/MM\/yyyy HH:mm:ss',   'ar-SA': 'dd\/MM\/yyyy \u0647\u0647:s\u0633',   'bg-BG': 'd.M.yyyy H:m:s \u0447.',   'ca-ES': 'dd\/MM\/yyyy H:mm:ss',   'cs-CZ': 'd.M.yyyy H:mm:ss',   'da-DK': 'dd-MM-yyyy HH:mm:ss',   'de-DE': 'dd.MM.yyyy HH:mm:ss',   'el-GR': 'd\/M\/yyyy h:mm:ss \u03c0\u03bc|\u03bc\u03bc',   'es-MX': 'dd\/MM\/yyyy H:mm:ss',   'fi-FI': 'd.M.yyyy klo H.mm.ss',   'fr-FR': 'dd\/MM\/yyyy HH:mm:ss',   'he-IL': 'dd\/MM\/yyyy HH:mm:ss',   'hi-IN': 'dd-MM-yyyy hh:mm:ss \u092c\u091c\u0947',   'hr-HR': 'd.M.yyyy. H:mm:ss',   'hu-HU': 'yyyy.MM.dd. H:mm:ss',   'id-ID': 'dd\/MM\/yyyy HH:mm:ss',   'is-IS': 'd.M.yyyy kl. HH:mm:ss',   'it-IT': 'dd\/MM\/yyyy HH:mm:ss',   'ja-JP': 'yyyy\/MM\/dd HH:mm:ss',   'ko-KR': 'yyyy\ub144 MM\uc6d4 dd\uc77c HH\uc2dc mm\ubd84 ss\ucd08',   'lt-LT': 'yyyy.MM.dd. HH:mm:ss',   'lv-LV': 'yyyy.gada MM.m\u0113nesis dd.diena HH:mm:ss',   'ms-MY': 'dd\/MM\/yyyy HH:mm:ss',   'nl-NL': 'dd-MM-yyyy HH:mm:ss',   'no-NO': 'dd.MM.yyyy HH:mm:ss',   'pl-PL': 'dd.MM.yyyy HH:mm:ss',   'pt-BR': 'dd\/MM\/yyyy HH:mm:ss',   'ro-RO': 'dd.MM.yyyy HH:mm:ss',   'ru-RU': 'dd.MM.yyyy HH:mm:ss',   'sk-SK': 'd. M. yyyy H:mm:ss',   'sl-SI': 'd.M.yyyy H:mm:ss',   'sr-RS': 'dd.MM.yyyy. HH:mm:ss',   'sv-SE': 'yyyy-MM-dd HH:mm:ss',   'th-TH': '\u0e27\u0e31\u0e19\u0e17\u0e35\u0e48 d \u0e40\u0e14\u0e37\u0e2d\u0e19 M \u0e1b\u0e35 yyyy \u0e40\u0e27\u0e25\u0e32 H:mm:ss',   'tr-TR': 'dd.MM.yyyy HH:mm:ss',   'uk-UA': 'dd.MM.yyyy HH:mm:ss',   'vi-VN': 'dd\/MM\/yyyy HH:mm:ss',   'zh-CN': 'yyyy\u5e74MM\u6708dd\u65e5 HH\u65f6mm\u5206ss\u79d2',   'zh-TW': 'yyyy\u5e74MM\u6708dd\u65e5 HH\u6642mm\u5206ss\u79d2', }; <\/code><\/pre>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043c \u043d\u043e\u0432\u044b\u0435 \u0442\u0438\u043f\u044b \u0432 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0432\u043f\u043e\u0441\u043b\u0435\u0434\u0441\u0442\u0432\u0438\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u043c \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/common-angular\/src\/lib\/formly\/formly-fields.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { TypeOption } from '@ngx-formly\/core\/lib\/models'; import { DateInputComponent } from '.\/date-input.component';  export const COMMON_FORMLY_FIELDS: TypeOption[] = [   {     name: 'date-input',     component: DateInputComponent,     extends: 'input',   }, ]; <\/code><\/pre>\n<h4>8. \u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0434\u043b\u044f \u0441\u043c\u0435\u043d\u044b \u043b\u043e\u043a\u0430\u043b\u0438 \u0432 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u0445 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h4>\n<p>\u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0440\u0430\u0437\u043d\u044b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0441\u0432\u043e\u0438 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u044b \u0434\u043b\u044f \u0441\u043c\u0435\u043d\u044b \u044f\u0437\u044b\u043a\u0430, \u043c\u044b \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0438\u043c \u0438\u0445 \u0432 \u0435\u0434\u0438\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 \u0438 \u043c\u0435\u0442\u043e\u0434.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/common-angular\/src\/lib\/services\/active-lang.service.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Inject, Injectable } from '@angular\/core'; import { toCamelCase, TranslocoService } from '@jsverse\/transloco'; import { LangToLocaleMapping, TRANSLOCO_LOCALE_LANG_MAPPING, TranslocoLocaleService } from '@jsverse\/transloco-locale'; import * as dateFnsLocales from 'date-fns\/locale'; import * as ngZorroLocales from 'ng-zorro-antd\/i18n'; import { NzI18nService } from 'ng-zorro-antd\/i18n';  @Injectable({ providedIn: 'root' }) export class ActiveLangService {   constructor(     private readonly translocoService: TranslocoService,     private readonly translocoLocaleService: TranslocoLocaleService,     private readonly nzI18nService: NzI18nService,     @Inject(TRANSLOCO_LOCALE_LANG_MAPPING)     readonly langToLocaleMapping: LangToLocaleMapping   ) {}    applyActiveLang(lang: string) {     const { locale, localeInSnakeCase, localeInCamelCase } = this.normalizeLangKey(lang);      this.translocoService.setActiveLang(lang);     this.translocoLocaleService.setLocale(locale);      if (ngZorroLocales[localeInSnakeCase]) {       this.nzI18nService.setLocale(ngZorroLocales[localeInSnakeCase]);     }      if (dateFnsLocales[lang]) {       this.nzI18nService.setDateLocale(dateFnsLocales[lang]);     }     if (dateFnsLocales[localeInCamelCase]) {       this.nzI18nService.setDateLocale(dateFnsLocales[localeInCamelCase]);     }   }    normalizeLangKey(lang: string) {     const locale = this.langToLocaleMapping[lang];     const localeInCamelCase = toCamelCase(locale);     const localeInSnakeCase = locale.split('-').join('_');     return { locale, localeInSnakeCase, localeInCamelCase };   } } <\/code><\/pre>\n<h4>9. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0445 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043b\u043e\u043a\u0430\u043b\u0438 \u0432 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u0445 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0445 \u0441 \u0434\u0430\u0442\u0430\u043c\u0438<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u043c \u0432\u0441\u0451 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442\u044c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0435 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043b\u043e\u043a\u0430\u043b\u0438 \u0432 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u0445 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0434\u0430\u0442\u0430\u043c\u0438.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>apps\/client\/src\/app\/app.config.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { provideTranslocoMessageformat } from '@jsverse\/transloco-messageformat';  \/\/ ...  import { COMMON_FORMLY_FIELDS } from '@nestjs-mod-fullstack\/common-angular'; import { FILES_FORMLY_FIELDS } from '@nestjs-mod-fullstack\/files-angular';  \/\/ ...  export const appConfig = ({ authorizerURL, minioURL }: { authorizerURL: string; minioURL: string }): ApplicationConfig =&gt; {   return {     providers: [       \/\/ ...       importProvidersFrom(         \/\/ ...         FormlyModule.forRoot({           \/\/ &lt;--updates           types: [...FILES_FORMLY_FIELDS, ...COMMON_FORMLY_FIELDS],         })       ),       \/\/ ...       provideTranslocoLocale({         \/\/ &lt;--updates         defaultLocale: 'en-US',         langToLocaleMapping: {           en: 'en-US',           ru: 'ru-RU',         },       }),       provideTranslocoMessageformat({         \/\/ &lt;--updates         locales: ['en-US', 'ru-RU'],       }),       \/\/ ...     ],   }; }; <\/code><\/pre>\n<h4>10. \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0432\u0432\u043e\u0434\u0430 \u043d\u0430 \u0444\u0440\u043e\u043d\u0442\u0435 \u0432 \u043c\u043e\u0434\u0443\u043b\u0435 &#171;Webhook&#187;<\/h4>\n<p>\u041d\u043e\u0432\u043e\u0435 \u043f\u043e\u043b\u0435 \u0444\u043e\u0440\u043c\u044b \u043c\u043e\u0436\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u0430\u043a \u0432 \u0432\u0438\u0434\u0435 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 <code>type=input<\/code> \u0441 \u0442\u0438\u043f\u043e\u043c <code>props.type=datetime-local<\/code>, \u0442\u0430\u043a \u0438 \u0432 \u0432\u0438\u0434\u0435 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044f <code>type=date-input<\/code>.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook-angular\/src\/lib\/services\/webhook-form.service.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Injectable } from '@angular\/core'; import { TranslocoService } from '@jsverse\/transloco'; import { UpdateWebhookDtoInterface, ValidationErrorMetadataInterface, WebhookEventInterface, WebhookScalarFieldEnumInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { ValidationService } from '@nestjs-mod-fullstack\/common-angular'; import { UntilDestroy } from '@ngneat\/until-destroy'; import { FormlyFieldConfig } from '@ngx-formly\/core'; import { tap } from 'rxjs'; import { WebhookEventsService } from '.\/webhook-events.service';  @UntilDestroy() @Injectable({ providedIn: 'root' }) export class WebhookFormService {   protected events: WebhookEventInterface[] = [];    constructor(protected readonly webhookEventsService: WebhookEventsService, protected readonly translocoService: TranslocoService, protected readonly validationService: ValidationService) {}    init() {     return this.webhookEventsService.findMany().pipe(       tap((events) =&gt; {         this.events = events;       })     );   }    getFormlyFields(options?: {     \/\/ eslint-disable-next-line @typescript-eslint\/no-unused-vars     data?: UpdateWebhookDtoInterface;     errors?: ValidationErrorMetadataInterface[];   }): FormlyFieldConfig[] {     return this.validationService.appendServerErrorsAsValidatorsToFields(       [         {           key: WebhookScalarFieldEnumInterface.enabled,           type: 'checkbox',           validation: {             show: true,           },           props: {             label: this.translocoService.translate(`webhook.form.fields.enabled`),             placeholder: 'enabled',             required: true,           },         },         {           key: WebhookScalarFieldEnumInterface.endpoint,           type: 'input',           validation: {             show: true,           },           props: {             label: this.translocoService.translate(`webhook.form.fields.endpoint`),             placeholder: 'endpoint',             required: true,           },         },         {           key: WebhookScalarFieldEnumInterface.eventName,           type: 'select',           validation: {             show: true,           },           props: {             label: this.translocoService.translate(`webhook.form.fields.event-name`),             placeholder: 'eventName',             required: true,             options: (this.events || []).map((e) =&gt; ({               value: e.eventName,               label: `${e.eventName} - ${e.description}`,             })),           },         },         {           key: WebhookScalarFieldEnumInterface.headers,           type: 'textarea',           validation: {             show: true,           },           props: {             label: this.translocoService.translate(`webhook.form.fields.headers`),             placeholder: 'headers',           },         },         {           key: WebhookScalarFieldEnumInterface.requestTimeout,           type: 'input',           validation: {             show: true,           },           props: {             type: 'number',             label: this.translocoService.translate(`webhook.form.fields.request-timeout`),             placeholder: 'requestTimeout',             required: false,           },         },         {           key: WebhookScalarFieldEnumInterface.workUntilDate, \/\/ &lt;-- updates           type: 'date-input',           validation: {             show: true,           },           props: {             type: 'datetime-local',             label: this.translocoService.translate(`webhook.form.fields.work-until-date`),             placeholder: 'workUntilDate',             required: false,           },         },       ],       options?.errors || []     );   } } <\/code><\/pre>\n<p>\u0414\u043b\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043c\u0430\u043f\u043f\u0435\u0440\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u043e\u043f\u0438\u0448\u0435\u043c \u0432 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u0435\u0440\u0432\u0438\u0441\u0435.<\/p>\n<p>\u0423\u0447\u0438\u0442\u044b\u0432\u0430\u044f \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0435 \u0441\u043c\u0435\u0449\u0435\u043d\u0438\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0440\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441 \u0434\u0430\u0442\u043e\u0439, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u043e\u0439 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u0432 \u043e\u0431\u044a\u0435\u043a\u0442 \u0434\u0430\u0442\u044b \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0447\u0438\u0442\u044b\u0432\u0430\u0442\u044c \u0441\u043c\u0435\u0449\u0435\u043d\u0438\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook-angular\/src\/lib\/services\/webhook-mapper.service.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Injectable } from '@angular\/core'; import { WebhookInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { BROWSER_TIMEZONE_OFFSET, safeParseJson } from '@nestjs-mod-fullstack\/common-angular'; import { addHours, format } from 'date-fns';  export interface WebhookModel extends Partial&lt;Omit&lt;WebhookInterface, 'workUntilDate' | 'createdAt' | 'updatedAt' | 'headers'&gt;&gt; {   headers?: string | null;   createdAt?: Date | null;   updatedAt?: Date | null;   workUntilDate?: Date | null; }  @Injectable({ providedIn: 'root' }) export class WebhookMapperService {   toModel(item?: WebhookInterface): WebhookModel {     return {       ...item,       headers: item?.headers ? JSON.stringify(item.headers) : '',       requestTimeout: item?.requestTimeout ? +item.requestTimeout : null,       workUntilDate: item?.workUntilDate ? addHours(new Date(item.workUntilDate), BROWSER_TIMEZONE_OFFSET) : null,       createdAt: item?.createdAt ? addHours(new Date(item.createdAt), BROWSER_TIMEZONE_OFFSET) : null,       updatedAt: item?.updatedAt ? addHours(new Date(item.updatedAt), BROWSER_TIMEZONE_OFFSET) : null,     };   }    toForm(model: WebhookModel) {     return {       ...model,       requestTimeout: model.requestTimeout ? model.requestTimeout : '',       workUntilDate: model.workUntilDate ? format(model.workUntilDate, 'yyyy-MM-dd HH:mm:ss') : null,     };   }    toJson(data: WebhookModel) {     return {       enabled: data.enabled === true,       endpoint: data.endpoint || '',       eventName: data.eventName || '',       headers: data.headers ? safeParseJson(data.headers) : null,       requestTimeout: data.requestTimeout ? +data.requestTimeout : null,       workUntilDate: data.workUntilDate ? format(new Date(data.workUntilDate), 'yyyy-MM-dd HH:mm:ss') : undefined,     };   } } <\/code><\/pre>\n<h4>11. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0439\u043f\u0430 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0434\u0430\u0442 \u043d\u0430 \u0444\u0440\u043e\u043d\u0442\u0435<\/h4>\n<p>\u0412\u043e \u0432\u0441\u0435\u0445 \u043c\u0435\u0441\u0442\u0430\u0445, \u0433\u0434\u0435 \u043c\u044b \u0432\u044b\u0432\u043e\u0434\u0438\u043c \u0434\u0430\u0442\u0443, \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0447\u0435\u0440\u0435\u0437 \u043f\u0430\u0439\u043f.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0430\u0439\u043f\u0430 <em>apps\/client\/src\/app\/app.component.html<\/em><\/p>\n<pre><code>&lt;nz-layout class=\"layout\"&gt;   &lt;!-- ... --&gt;   &lt;nz-footer class=\"flex justify-between\"&gt;     &lt;!-- ... --&gt;     &lt;div id=\"serverTime\"&gt;{{ (serverTime$ | async)! | translocoDate : { dateStyle: 'medium', timeStyle: 'medium' } }}&lt;\/div&gt;   &lt;\/nz-footer&gt; &lt;\/nz-layout&gt; <\/code><\/pre>\n<h4>12. \u0410\u0434\u0430\u043f\u0442\u0430\u0446\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430<\/h4>\n<p>\u0414\u043e \u0441\u0438\u0445 \u043f\u043e\u0440 \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 \u043c\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u043b\u0438 \u0434\u0430\u0442\u044b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u043e\u043c \u0441 \u0431\u044d\u043a\u0435\u043d\u0434\u0430.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c, \u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u0432\u0441\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441 \u0434\u0430\u0442\u0430\u043c\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0430\u0434\u0430\u043f\u0442\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u043f\u043e\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u0421\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u0432\u0441\u0435 \u043d\u0430\u0448\u0438 \u0442\u0435\u0441\u0442\u044b, \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044e\u0449\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0435 \u0434\u0430\u0442\u044b, \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0438 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e.<\/p>\n<p>\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0432\u0435\u043b\u0438\u043a\u043e, \u043d\u043e \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u0430\u0434\u0430\u043f\u0442\u0430\u0446\u0438\u0438 \u0432\u0435\u0437\u0434\u0435 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u0430 <em>apps\/client-e2e\/src\/ru-example.spec.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { expect, Page, test } from '@playwright\/test'; import { join } from 'path'; import { setTimeout } from 'timers\/promises';  test.describe('basic usage (ru)', () =&gt; {   \/\/ ...    \/\/ &lt;-- updates   test('has serverTime format should be equal to \"21 \u0434\u0435\u043a. 2024 \u0433., 13:56:00\" without \"13:56:00\"', async () =&gt; {     await page.goto('\/', {       timeout: 7000,     });      await setTimeout(4000);      const serverTime = await page.locator('#serverTime').innerText();     expect(       serverTime         .split(' ')         .filter((p, i) =&gt; i !== 4)         .join(' ')     ).toEqual(       new Intl.DateTimeFormat('ru-RU', {         dateStyle: 'medium',         timeStyle: 'medium',       })         .format(new Date())         .split(' ')         .filter((p, i) =&gt; i !== 4)         .join(' ')     );   }); }); <\/code><\/pre>\n<h4>13. \u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u043e\u0432, \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043b\u043e\u0432\u0430\u0440\u0435\u0439 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043c \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u043e\u0432, \u043e\u0431\u043d\u043e\u0432\u0438\u043c \u0441\u043b\u043e\u0432\u0430\u0440\u0438 \u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u043c \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043c \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u0443\u044e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 E2E-\u0442\u0435\u0441\u0442\u044b.<\/p>\n<p><em>\u041a\u043e\u043c\u0430\u043d\u0434\u044b<\/em><\/p>\n<pre><code class=\"bash\">npm run manual:prepare npm run translates npm run pm2-full:dev:start npm run pm2-full:dev:test:e2e <\/code><\/pre>\n<h4>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h4>\n<p>\u0425\u043e\u0442\u044f \u043c\u043e\u044f \u0446\u0435\u043b\u044c \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u043b\u0430\u0441\u044c \u0432 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u043c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043a\u043e\u0434\u0430, \u0441\u043d\u043e\u0432\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u043e\u0431\u044a\u0451\u043c\u043d\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435, \u043d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0432\u0441\u0435\u0433\u043e \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0441 \u0442\u0438\u043f\u043e\u043c <code>Date<\/code>.<\/p>\n<p>\u041d\u043e\u0432\u044b\u0435 \u0442\u0438\u043f\u044b \u043f\u043e\u043b\u0435\u0439 \u043d\u0435 \u0442\u0430\u043a \u0447\u0430\u0441\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0447\u0430\u043b\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043e\u0431\u044b\u0447\u043d\u043e \u043f\u0440\u043e\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0442\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0430\u043d\u0430\u043b\u0438\u0437 \u0431\u0443\u0434\u0443\u0449\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u044e\u0442\u0441\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0442\u0438\u043f\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0432\u0432\u043e\u0434\u0430 \u0438 \u0432\u044b\u0432\u043e\u0434\u0430.<\/p>\n<p>\u041d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u043c\u0438 \u0442\u0438\u043f\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445: \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u0447\u0438\u0441\u043b\u0430, \u0441\u043b\u043e\u0432\u0430\u0440\u0438, \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0438, \u0444\u0430\u0439\u043b\u044b \u0438 \u0434\u0430\u0442\u0430-\u0432\u0440\u0435\u043c\u044f.<\/p>\n<p>\u042d\u0442\u0438 \u0442\u0438\u043f\u044b \u0432\u043f\u043e\u043b\u043d\u0435 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u044b \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 <code>CRM<\/code>-\u0441\u0438\u0441\u0442\u0435\u043c\u044b.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043a\u0430\u0441\u0442\u043e\u043c\u0438\u0437\u0430\u0446\u0438\u044f, \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f <code>\u0444\u0430\u0439\u043b<\/code> \u0438 <code>\u0434\u0430\u0442\u0430-\u0432\u0440\u0435\u043c\u044f<\/code>.<\/p>\n<h4>\u041f\u043b\u0430\u043d\u044b<\/h4>\n<p>\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0430\u0441\u043f\u0435\u043a\u0442\u044b \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0442\u0438\u043f\u043e\u0432\u043e\u0433\u043e <code>REST<\/code> \u043a\u043e\u0434\u0430 \u0443\u0436\u0435 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u044b \u0432 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445.<\/p>\n<p>\u042f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u043d\u0435 \u0437\u0430\u0442\u0440\u0430\u0433\u0438\u0432\u0430\u043b \u0432\u043e\u043f\u0440\u043e\u0441\u044b \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043e\u0447\u0435\u0440\u0435\u0434\u0435\u0439 \u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d\u0438 \u0437\u0430\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u044e\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u0446\u0438\u043a\u043b\u043e\u0432 \u0441\u0442\u0430\u0442\u0435\u0439, \u043d\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0442\u0435\u043a\u0443\u0449\u0438\u043c <code>REST<\/code> \u0431\u043e\u0439\u043b\u0435\u0440\u043f\u043b\u0435\u0439\u0442\u043e\u043c.<\/p>\n<p>\u041d\u0430 \u0441\u0435\u0433\u043e\u0434\u043d\u044f\u0448\u043d\u0438\u0439 \u0434\u0435\u043d\u044c \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d \u0447\u0430\u0441\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0436\u0435\u0441\u0442\u043a\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u0430 \u0441 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c <code>Docker<\/code>-\u043e\u0431\u0440\u0430\u0437\u043e\u0432 \u0438 \u0440\u0430\u0437\u0432\u0435\u0440\u0442\u044b\u0432\u0430\u043d\u0438\u0435\u043c \u0432 <code>Kubernetes<\/code>, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u043e\u0439 \u0438\u0437\u0431\u044b\u0442\u043e\u0447\u043d\u0443\u044e \u0441\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043c\u043d\u043e\u0433\u0438\u0445 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430 \u0438 \u0431\u044d\u043a\u0435\u043d\u0434\u0430.<\/p>\n<p>\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043f\u043e\u0441\u0442\u0430\u0440\u0430\u044e\u0441\u044c \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u0443\u043f\u0440\u043e\u0449\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043a <code>DevOps<\/code>, \u043e\u0440\u0438\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0435 \u0438\u043b\u0438 \u0443\u0441\u043b\u043e\u0432\u043d\u043e-\u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u044f.<\/p>\n<h4>\u0421\u0441\u044b\u043b\u043a\u0438<\/h4>\n<ul>\n<li>\n<p><a href=\"https:\/\/nestjs.com\" rel=\"noopener noreferrer nofollow\">https:\/\/nestjs.com<\/a> &#8212; \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0430\u0439\u0442 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/nestjs-mod.com\" rel=\"noopener noreferrer nofollow\">https:\/\/nestjs-mod.com<\/a> &#8212; \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0430\u0439\u0442 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0443\u0442\u0438\u043b\u0438\u0442<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/fullstack.nestjs-mod.com\" rel=\"noopener noreferrer nofollow\">https:\/\/fullstack.nestjs-mod.com<\/a> &#8212; \u0441\u0430\u0439\u0442 \u0438\u0437 \u043f\u043e\u0441\u0442\u0430<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack<\/a> &#8212; \u043f\u0440\u043e\u0435\u043a\u0442 \u0438\u0437 \u043f\u043e\u0441\u0442\u0430<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/compare\/4f495dbd6b9b4efd8d8e13a60c5f66b895c483af..ac8ce1e94a24f912f73c5eb1950458ebc77c12d4\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/compare\/4f495dbd6b9b4efd8d8e13a60c5f66b895c483af..ac8ce1e94a24f912f73c5eb1950458ebc77c12d4<\/a> &#8212; \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/actions\/runs\/12537857829\/artifacts\/2369701323\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/actions\/runs\/12537857829\/artifacts\/2369701323<\/a> &#8212; \u0432\u0438\u0434\u0435\u043e \u0441 E2E-\u0442\u0435\u0441\u0442\u043e\u0432 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430<\/p>\n<\/li>\n<\/ul>\n<h4>P.S.<\/h4>\n<p>\u0421 \u043d\u0430\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u043c \u041d\u043e\u0432\u044b\u043c 2025 \u0433\u043e\u0434\u043e\u043c! \u0416\u0435\u043b\u0430\u044e \u0432\u0441\u0435\u043c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u044f, \u043b\u044e\u0431\u0432\u0438 \u0438 \u0443\u0434\u0430\u0447\u0438! \ud83e\udd73\ud83e\udd73\ud83e\udd73<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/870854\/\"> https:\/\/habr.com\/ru\/articles\/870854\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><strong>\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f:<\/strong> <a href=\"https:\/\/habr.com\/ru\/articles\/866858\/\" rel=\"noopener noreferrer nofollow\">\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u0444\u0443\u043b\u0441\u0442\u0435\u043a-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u043d\u0430 &#171;Angular&#187; \u0438 &#171;NestJS&#187;<\/a><\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0438 \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044f <code>workUntilDate<\/code> \u0441 \u0442\u0438\u043f\u043e\u043c <code>timestamp(6)<\/code> \u0432 \u0442\u0430\u0431\u043b\u0438\u0446\u0443 <code>Webhook<\/code> \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 <code>Webhook<\/code>.<\/p>\n<p>\u041d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430 (\u0432 <code>Angular<\/code>-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438) \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0431\u0443\u0434\u0435\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0443\u0434\u043e\u0431\u043d\u044b\u0439 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044c \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0432\u044b\u0431\u043e\u0440\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438.<\/p>\n<p>\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0441\u043c\u043e\u0433\u0443\u0442 \u0437\u0430\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0443 \u0438 \u0432\u0440\u0435\u043c\u044f \u0432 \u0441\u0432\u043e\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u0435, \u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a \u0431\u044d\u043a\u0435\u043d\u0434 (<code>NestJS<\/code>-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435) \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0432\u0432\u0435\u0434\u0451\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 <code>UTC+0<\/code>.<\/p>\n<p>\u041a\u0440\u043e\u043c\u0435 \u0442\u043e\u0433\u043e, \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044f \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0435 \u0434\u0430\u0442\u044b, \u0431\u0443\u0434\u0443\u0442 \u0430\u0434\u0430\u043f\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u043f\u043e\u0434 \u044f\u0437\u044b\u043a \u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u0437\u043e\u043d\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<h4>1. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a<\/h4>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u044b\u0435 \u043f\u0430\u043a\u0435\u0442\u044b:<\/p>\n<p><em>\u041a\u043e\u043c\u0430\u043d\u0434\u044b<\/em><\/p>\n<pre><code class=\"bash\">npm install --save @jsverse\/transloco-locale @jsverse\/transloco-messageformat --prefer-offline --no-audit --progress=false <\/code><\/pre>\n<h4>2. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438<\/h4>\n<p>\u041c\u043e\u0438 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u044b \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0445 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e.<\/p>\n<p>\u042d\u0442\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0432 \u0442\u0435\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445, \u043a\u043e\u0433\u0434\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0435\u0451 \u0437\u0430\u043d\u043e\u0432\u043e.<\/p>\n<p><em>\u041a\u043e\u043c\u0430\u043d\u0434\u044b<\/em><\/p>\n<pre><code class=\"bash\">npm run flyway:create:webhook --args=AddFieldWorkUntilDateToAuthUser <\/code><\/pre>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook\/src\/migrations\/V202412200905__AddFieldWorkUntilDateToAuthUser.sql<\/em><\/p>\n<pre><code class=\"sql\">DO $$ BEGIN     ALTER TABLE \"Webhook\"         ADD \"workUntilDate\" timestamp(6); EXCEPTION     WHEN duplicate_column THEN         NULL; END $$;   <\/code><\/pre>\n<h4>3. \u041f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 &#171;Prisma&#187;-\u0441\u0445\u0435\u043c<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0443\u044e \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044e, \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u0445\u0435\u043c\u044b <code>Prisma<\/code> \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043c <code>Prisma<\/code>-\u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440\u044b.<\/p>\n<p><em>\u041a\u043e\u043c\u0430\u043d\u0434\u044b<\/em><\/p>\n<pre><code class=\"bash\">npm run docker-compose:start-prod:server npm run db:create-and-fill npm run prisma:pull npm run generate <\/code><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u044d\u0442\u0438\u0445 \u0448\u0430\u0433\u043e\u0432, \u0432\u043e \u0432\u0441\u0435\u0445 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 <code>DTO<\/code> \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u043d\u043e\u0432\u043e\u0435 \u043f\u043e\u043b\u0435 <code>workUntilDate<\/code>.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f <code>DTO<\/code>-\u0444\u0430\u0439\u043b\u0430 <em>libs\/feature\/webhook\/src\/lib\/generated\/rest\/dto\/webhook.dto.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Prisma } from '..\/..\/..\/..\/..\/..\/..\/..\/node_modules\/@prisma\/webhook-client'; import { ApiProperty } from '@nestjs\/swagger';  export class WebhookDto {   \/\/ ...   \/\/ updates   @ApiProperty({     type: 'string',     format: 'date-time',     nullable: true,   })   workUntilDate!: Date | null; } <\/code><\/pre>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f <code>Prisma<\/code>-\u0441\u0445\u0435\u043c\u044b <em>libs\/feature\/webhook\/src\/prisma\/schema.prisma<\/em><\/p>\n<pre><code>generator client {   provider = \"prisma-client-js\"   engineType = \"binary\"   output   = \"..\/..\/..\/..\/..\/node_modules\/@prisma\/webhook-client\"   binaryTargets = [\"native\",\"linux-musl\",\"debian-openssl-1.1.x\",\"linux-musl-openssl-3.0.x\"] }  \/\/ ...  model Webhook {   id                                         String       @id(map: \"PK_WEBHOOK\") @default(dbgenerated(\"uuid_generate_v4()\")) @db.Uuid   \/\/ ...   workUntilDate                              DateTime?    @db.Timestamp(6) \/\/\/ &lt;-- updates } <\/code><\/pre>\n<h4>4. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 &#171;AsyncLocalStorage&#187; \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f<\/h4>\n<p>\u0420\u0430\u043d\u0435\u0435 \u043c\u044b \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u043b\u0438 <code>AuthTimezoneInterceptor<\/code> \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0434\u0430\u0442\u0430\u043c\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 <code>UTC-0<\/code> \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0439 \u0434\u0430\u0442\u044b \u0438\u0437 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u0434\u0430\u0442\u0443 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 <code>UTC-0<\/code>, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043e\u043d\u0430 \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 <code>AuthTimezonePipe<\/code>.<\/p>\n<p>\u041e\u0434\u043d\u0430\u043a\u043e \u0432 \u044d\u0442\u043e\u043c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435 \u0443 \u043d\u0430\u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u0430\u043d\u043d\u044b\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u0435\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u0437\u043e\u043d\u0443.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0440\u0435\u0448\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u043c\u044b \u043e\u0431\u0435\u0440\u043d\u0435\u043c \u043a\u0430\u0436\u0434\u044b\u0439 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432 <code>AsyncLocalStorage<\/code>, \u0447\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/core\/auth\/src\/lib\/interceptors\/auth-timezone.interceptor.ts<\/em><\/p>\n<pre><code class=\"typescript\">\/\/ ... import { AsyncLocalStorage } from 'node:async_hooks'; import { AuthAsyncLocalStorageData } from '..\/types\/auth-async-local-storage-data';  @Injectable() export class AuthTimezoneInterceptor implements NestInterceptor&lt;TData, TData&gt; {   constructor(     \/\/ ...     private readonly asyncLocalStorage: AsyncLocalStorage&lt;AuthAsyncLocalStorageData&gt;   ) {}    intercept(context: ExecutionContext, next: CallHandler) {     const req: AuthRequest = getRequestFromExecutionContext(context);     const userId = req.authUser?.externalUserId;      if (!this.authEnvironments.useInterceptors) {       return next.handle();     }      if (!userId) {       return next.handle();     }      const run = () =&gt; {       const result = next.handle();        if (isObservable(result)) {         return result.pipe(           concatMap(async (data) =&gt; {             const user = await this.authCacheService.getCachedUserByExternalUserId(userId);             return this.authTimezoneService.convertObject(data, user?.timezone);           })         );       }       if (result instanceof Promise &amp;&amp; typeof result?.then === 'function') {         return result.then(async (data) =&gt; {           if (isObservable(data)) {             return data.pipe(               concatMap(async (data) =&gt; {                 const user = await this.authCacheService.getCachedUserByExternalUserId(userId);                 return this.authTimezoneService.convertObject(data, user?.timezone);               })             );           } else {             const user = await this.authCacheService.getCachedUserByExternalUserId(userId);             \/\/ need for correct map types with base method of NestInterceptor             return this.authTimezoneService.convertObject(data, user?.timezone) as Observable&lt;TData&gt;;           }         });       }       \/\/ need for correct map types with base method of NestInterceptor       return this.authTimezoneService.convertObject(result, req.authUser?.timezone) as Observable&lt;TData&gt;;     };      if (!this.authEnvironments.usePipes) {       return run();     }      return this.asyncLocalStorage.run({ authTimezone: req.authUser?.timezone || 0 }, () =&gt; run());   } } <\/code><\/pre>\n<h4>5. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 &#171;Pipe&#187; \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430<\/h4>\n<p>\u041c\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c <code>Pipe<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0447\u0438\u0442\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u0437\u043e\u043d\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u0437 \u0432\u0441\u0435\u0445 \u043f\u043e\u043b\u0435\u0439 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0445 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441 \u0434\u0430\u0442\u0430\u043c\u0438.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u0437\u043e\u043d\u0430 \u0441\u0430\u043c\u043e\u0433\u043e \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 <code>UTC-0<\/code>, \u0442\u043e \u043e\u0442\u043d\u0438\u043c\u0430\u0435\u043c \u0440\u0430\u0437\u043d\u0438\u0446\u0443.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/core\/auth\/src\/lib\/pipes\/auth-timezone.pipe.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { SERVER_TIMEZONE_OFFSET } from '@nestjs-mod-fullstack\/common'; import { Injectable, PipeTransform } from '@nestjs\/common'; import { AsyncLocalStorage } from 'node:async_hooks'; import { AuthEnvironments } from '..\/auth.environments'; import { AuthTimezoneService } from '..\/services\/auth-timezone.service'; import { AuthAsyncLocalStorageData } from '..\/types\/auth-async-local-storage-data';  @Injectable() export class AuthTimezonePipe implements PipeTransform {   constructor(private readonly asyncLocalStorage: AsyncLocalStorage&lt;AuthAsyncLocalStorageData&gt;, private readonly authTimezoneService: AuthTimezoneService, private readonly authEnvironments: AuthEnvironments) {}    transform(value: unknown) {     if (!this.authEnvironments.usePipes) {       return value;     }     const result = this.authTimezoneService.convertObject(value, -1 * (this.asyncLocalStorage.getStore()?.authTimezone || 0) - SERVER_TIMEZONE_OFFSET);     return result;   } } <\/code><\/pre>\n<h4>6. \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0446\u0435\u043f\u0442\u043e\u0440\u0430 \u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u043c\u043e\u0434\u0443\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0446\u0435\u043f\u0442\u043e\u0440 \u0438 \u0441\u0435\u0440\u0432\u0438\u0441 \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u043c\u043e\u0434\u0443\u043b\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/core\/auth\/src\/lib\/auth.module.ts<\/em><\/p>\n<pre><code class=\"typescript\">\/\/ ... import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs\/core'; \/\/ ... import { AsyncLocalStorage } from 'node:async_hooks'; import { AuthTimezonePipe } from '.\/pipes\/auth-timezone.pipe';  export const { AuthModule } = createNestModule({   \/\/ ...   sharedProviders: [     {       provide: AsyncLocalStorage,       useValue: new AsyncLocalStorage(),     },     AuthTimezoneService,     AuthCacheService,   ],   providers: [     \/\/ ...     { provide: APP_PIPE, useClass: AuthTimezonePipe },     AuthAuthorizerService,     AuthAuthorizerBootstrapService,   ],   \/\/ ... }); <\/code><\/pre>\n<h4>7. \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u043f\u043e\u043b\u044f &#171;date-input&#187; \u0434\u043b\u044f &#171;Formly&#187;<\/h4>\n<p>\u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0435 <code>HTML<\/code>-\u043f\u043e\u043b\u0435 \u0432\u0432\u043e\u0434\u0430 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0432\u043e\u0434 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0442\u0438\u043f\u043e\u043c <code>Date<\/code>, \u0435\u0433\u043e \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0432\u0438\u0434 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445 <code>ng.ant.design<\/code>.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0435\u0434\u0438\u043d\u043e\u043e\u0431\u0440\u0430\u0437\u0438\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430, \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u043e\u0432\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b <code>date-input<\/code> \u0434\u043b\u044f <code>Formly<\/code>.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/common-angular\/src\/lib\/formly\/date-input.component.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { AsyncPipe } from '@angular\/common'; import { ChangeDetectionStrategy, Component } from '@angular\/core'; import { ReactiveFormsModule } from '@angular\/forms'; import { TranslocoService } from '@jsverse\/transloco'; import { FieldType, FieldTypeConfig, FormlyModule } from '@ngx-formly\/core'; import { NzDatePickerModule } from 'ng-zorro-antd\/date-picker'; import { map, Observable } from 'rxjs'; import { DATE_INPUT_FORMATS } from '..\/constants\/date-input-formats'; import { ActiveLangService } from '..\/services\/active-lang.service';  @Component({   selector: 'date-input',   standalone: true,   changeDetection: ChangeDetectionStrategy.OnPush,   imports: [ReactiveFormsModule, FormlyModule, NzDatePickerModule, AsyncPipe],   template: ` &lt;nz-date-picker [formControl]=\"formControl\" [formlyAttributes]=\"field\" [nzShowTime]=\"true\" [nzFormat]=\"(format$ | async)!\"&gt;&lt;\/nz-date-picker&gt; `, }) export class DateInputComponent extends FieldType&lt;FieldTypeConfig&gt; {   format$: Observable&lt;string&gt;;    constructor(private readonly translocoService: TranslocoService, private readonly activeLangService: ActiveLangService) {     super();     this.format$ = translocoService.langChanges$.pipe(       map((lang) =&gt; {         const { locale } = this.activeLangService.normalizeLangKey(lang);         return DATE_INPUT_FORMATS[locale] ? DATE_INPUT_FORMATS[locale] : DATE_INPUT_FORMATS['en-US'];       })     );   } } <\/code><\/pre>\n<p>\u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044c \u0442\u0435\u043f\u0435\u0440\u044c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438, \u043e\u0434\u043d\u0430\u043a\u043e \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u0441\u0430\u043c\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0432\u0432\u043e\u0434\u0430 \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043d\u0435\u0438\u0437\u043c\u0435\u043d\u043d\u044b\u043c.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0440\u0435\u0448\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u043f\u0438\u0441\u043e\u043a \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043b\u043e\u043a\u0430\u043b\u0435\u0439 \u0438 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432 \u0432\u044b\u0432\u043e\u0434\u0430 \u0438 \u043d\u0430\u0441\u0442\u0430\u0440\u0438\u0432\u0430\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u044b\u0432\u043e\u0434\u0430 \u0434\u0430\u0442\u044b \u0432 <code>input<\/code>.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/common-angular\/src\/lib\/constants\/date-input-formats.ts<\/em><\/p>\n<pre><code class=\"typescript\">export const DATE_INPUT_FORMATS = {   'en-US': 'MM\/dd\/yyyy HH:mm:ss',   'en-GB': 'dd\/MM\/yyyy HH:mm:ss',  <\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-443853","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/443853","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=443853"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/443853\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=443853"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=443853"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=443853"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}