first commit: منصة Tripz — خطط كاملة + سكافولد باك إند NestJS/Docker

- docs/00-15: دراسة، بنية، محرك تعرفة، تسعير، نموذج استئجار، تكاملات، بيانات، realtime، خطة، devops، لاندنج، مخاطر، اصطلاحات سيرفر، تدفق نشر
- backend/: NestJS 11 على Docker (health + tenants + عزل tenant_id + بادئة tripz_ + Redis DB 3)
- apps/rider, apps/driver, dashboards/admin-web, dashboards/superadmin-web (هياكل)
- sync-to-server.sh + .gitignore

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 15:22:06 +03:00
co-authored by Claude Opus 4.8
commit 95fea546f5
49 changed files with 1881 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules
dist
npm-debug.log
.env
.git
.gitignore
test
**/*.spec.ts
+32
View File
@@ -0,0 +1,32 @@
# ==== Tripz backend env (شغّل: cp .env.example .env) ====
# ملاحظة: السيرفر مشترك — كل شيء معزول ببادئة tripz و Redis DB غير الافتراضي (راجع docs/14)
NODE_ENV=development
API_PORT=4010 # منفذ غير شائع لتفادي التصادم على السيرفر المشترك
# ---- PostgreSQL (قاعدة مستقلة + بادئة جداول) ----
DB_HOST=postgres
DB_PORT=5432
DB_NAME=tripz
DB_USER=tripz
DB_PASSWORD=change_me_strong
DB_TABLE_PREFIX=tripz_
DB_SYNC=false # لا synchronize في الإنتاج — هجرات فقط
# ---- Redis (DB رقم 3 غير الافتراضي 0 + بادئة مفاتيح) ----
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=3
REDIS_KEY_PREFIX=tripz:
# ---- BullMQ ----
QUEUE_PREFIX=tripz_
# ---- Auth ----
JWT_SECRET=change_me_jwt_secret
JWT_EXPIRES=15m
JWT_REFRESH_EXPIRES=30d
# ---- Maps (انطلق) ----
MAPS_TILES_URL=http://martin:3000
MAPS_PROVIDER=antlaq
+21
View File
@@ -0,0 +1,21 @@
# ==== Tripz backend — multi-stage Docker build ====
# البناء والتشغيل كله داخل Docker (لا تثبيت محلي على الماك)
# ---- builder ----
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# ---- runtime ----
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm install --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 4010
# نقطة الدخول الافتراضية: الـ API. الـ worker يُشغَّل بأمر مختلف من compose.
CMD ["node", "dist/main.js"]
+48
View File
@@ -0,0 +1,48 @@
# Tripz Backend — NestJS (Docker فقط)
> **لا تثبيت محلي على الماك.** الكود يُكتب محلياً، والبناء والتشغيل والهجرات **كلها داخل Docker على السيرفر**.
## التشغيل على السيرفر
```bash
cd backend
cp .env.example .env # ثم عدّل كلمات المرور والأسرار
docker compose up -d --build # يبني ويشغّل: postgres/postgis, redis, martin, api, worker
```
## الهجرات (داخل حاوية الـ api)
```bash
docker compose exec api npm run migration:run
```
## فحص سريع
```bash
curl http://<server>:4010/api/health # {"status":"ok",...}
# توثيق Swagger: http://<server>:4010/api/docs
```
## العزل على السيرفر المشترك (راجع docs/14)
- Postgres: قاعدة `tripz`، بادئة جداول `tripz_` (مثال: `tripz_tenants`).
- Redis: **DB رقم 3** غير الافتراضي + بادئة مفاتيح `tripz:`.
- منافذ مضيف غير قياسية (API 4010، Postgres 55432) لتفادي التصادم.
- أسماء حاويات/شبكة/فوليوم ببادئة `tripz-`.
## البنية الحالية (P0)
```
src/
├── main.ts # bootstrap API (+ Swagger)
├── worker.ts # نقطة دخول BullMQ (هيكل)
├── app.module.ts # ConfigModule + TypeORM(entityPrefix) + Throttler + TenantMiddleware
├── config/ # configuration.ts + data-source.ts (هجرات)
├── common/
│ ├── tenant/ # context (AsyncLocalStorage) + middleware + @Tenant()
│ └── usage/ # UsageInterceptor (أساس الفوترة)
├── database/
│ ├── entities/ # tenant.entity.ts
│ └── migrations/ # InitTenants
└── modules/
├── health/ # GET /api/health
└── tenants/ # GET /api/tenant/config/:slug ، admin/tenants
```
## التالي (P1)
users · auth(OTP) · drivers · trips(★ آلة حالة) · matching(Redis GEO) · tariff · realtime(Socket.IO) · maps(وكيل انطلق).
+74
View File
@@ -0,0 +1,74 @@
# ==== Tripz stack — كل شيء معزول ببادئة tripz على السيرفر المشترك ====
# التشغيل على السيرفر: docker compose up -d --build
# أسماء الحاويات/الشبكة/الفوليوم كلها ببادئة tripz لتفادي التصادم.
name: tripz
services:
postgres:
image: postgis/postgis:16-3.4
container_name: tripz-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-tripz}
POSTGRES_USER: ${DB_USER:-tripz}
POSTGRES_PASSWORD: ${DB_PASSWORD:-change_me_strong}
volumes:
- tripz-pgdata:/var/lib/postgresql/data
ports:
- "${DB_EXPOSE_PORT:-55432}:5432" # منفذ مضيف غير قياسي (عزل)
networks: [tripz-net]
redis:
image: redis:7-alpine
container_name: tripz-redis
restart: unless-stopped
# ملاحظة: نعزل منطقياً بـ DB index 3 + keyPrefix، لا بنسخة redis منفصلة.
command: ["redis-server", "--save", "60", "1"]
volumes:
- tripz-redisdata:/data
networks: [tripz-net]
martin:
image: ghcr.io/maplibre/martin:latest
container_name: tripz-martin
restart: unless-stopped
# خادم بلاطات انطلق (يُهيّأ لاحقاً بمصدر البيانات). موجود من الآن للربط.
environment:
DATABASE_URL: postgres://${DB_USER:-tripz}:${DB_PASSWORD:-change_me_strong}@postgres:5432/${DB_NAME:-tripz}
depends_on: [postgres]
networks: [tripz-net]
api:
build: .
container_name: tripz-api
restart: unless-stopped
env_file: .env
environment:
DB_HOST: postgres
REDIS_HOST: redis
command: ["node", "dist/main.js"]
depends_on: [postgres, redis]
ports:
- "${API_PORT:-4010}:4010"
networks: [tripz-net]
worker:
build: .
container_name: tripz-worker
restart: unless-stopped
env_file: .env
environment:
DB_HOST: postgres
REDIS_HOST: redis
command: ["node", "dist/worker.js"]
depends_on: [postgres, redis]
networks: [tripz-net]
volumes:
tripz-pgdata:
tripz-redisdata:
networks:
tripz-net:
name: tripz-net
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+53
View File
@@ -0,0 +1,53 @@
{
"name": "tripz-backend",
"version": "0.1.0",
"description": "Tripz — multi-tenant ride-hailing platform API (NestJS)",
"private": true,
"scripts": {
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main.js",
"worker": "node dist/worker.js",
"typeorm": "typeorm-ts-node-commonjs -d src/config/data-source.ts",
"migration:generate": "npm run typeorm -- migration:generate",
"migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert",
"lint": "eslint \"src/**/*.ts\" --fix",
"test": "jest"
},
"dependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/config": "^4.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"@nestjs/platform-socket.io": "^11.0.0",
"@nestjs/swagger": "^11.0.0",
"@nestjs/throttler": "^6.2.0",
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.0",
"bullmq": "^5.12.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"ioredis": "^5.4.1",
"pg": "^8.12.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.7.5",
"typeorm": "^0.3.20"
},
"devDependencies": {
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.12",
"@types/node": "^22.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.0",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typescript": "^5.5.0"
}
}
+40
View File
@@ -0,0 +1,40 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import configuration from './config/configuration';
import { TenantMiddleware } from './common/tenant/tenant.middleware';
import { HealthModule } from './modules/health/health.module';
import { TenantsModule } from './modules/tenants/tenants.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get<string>('db.host'),
port: cfg.get<number>('db.port'),
database: cfg.get<string>('db.name'),
username: cfg.get<string>('db.user'),
password: cfg.get<string>('db.password'),
// بادئة الجداول (tripz_) لعزل السيرفر المشترك — راجع docs/14
entityPrefix: cfg.get<string>('db.tablePrefix'),
synchronize: cfg.get<boolean>('db.synchronize'),
autoLoadEntities: true,
}),
}),
ThrottlerModule.forRoot([{ ttl: 60000, limit: 120 }]),
HealthModule,
TenantsModule,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TenantMiddleware).forRoutes('*');
}
}
@@ -0,0 +1,17 @@
import { AsyncLocalStorage } from 'async_hooks';
export interface TenantStore {
tenantId: string;
userId?: string;
role?: string;
}
/**
* سياق المستأجر لكل طلب — يُملأ من TenantMiddleware ويُقرأ في المستودعات
* لفرض النطاق على tenant_id تلقائياً (راجع docs/06).
*/
export const tenantContext = new AsyncLocalStorage<TenantStore>();
export function currentTenantId(): string | undefined {
return tenantContext.getStore()?.tenantId;
}
@@ -0,0 +1,10 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { currentTenantId } from './tenant.context';
/**
* @Tenant() — يحقن معرّف المستأجر الحالي في معاملات المتحكّم.
*/
export const Tenant = createParamDecorator(
(_data: unknown, _ctx: ExecutionContext): string | undefined =>
currentTenantId(),
);
@@ -0,0 +1,20 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { tenantContext } from './tenant.context';
/**
* يستخرج معرّف المستأجر من الترويسة (أو الـ subdomain لاحقاً) ويضعه في السياق
* لبقية دورة حياة الطلب. لا استعلام يمر بلا tenantId (راجع docs/06).
*/
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {
const headerTenant =
(req.headers['x-tenant-id'] as string) ||
(req.headers['x-tenant'] as string) ||
'';
// السوبر-آدمن قد لا يحمل مستأجراً محدداً — يُعالَج بحارس منفصل لاحقاً.
tenantContext.run({ tenantId: headerTenant }, () => next());
}
}
@@ -0,0 +1,30 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { currentTenantId } from '../tenant/tenant.context';
/**
* يقيس الاستخدام لكل مستأجر (أساس الفوترة الشهرية — راجع docs/05).
* حالياً يسجّل فقط؛ لاحقاً يكتب في جدول usage / يدفع لطابور BullMQ.
*/
@Injectable()
export class UsageInterceptor implements NestInterceptor {
private readonly logger = new Logger('Usage');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const tenantId = currentTenantId() ?? 'none';
return next.handle().pipe(
tap(() => {
// TODO(P1): عدّ الرحلات المكتملة و GMV بدل تسجيل كل طلب.
this.logger.debug(`tenant=${tenantId} ${req.method} ${req.url}`);
}),
);
}
}
+41
View File
@@ -0,0 +1,41 @@
/**
* إعداد مركزي يقرأ متغيرات البيئة مع افتراضات العزل (راجع docs/14).
*/
export default () => ({
env: process.env.NODE_ENV ?? 'development',
apiPort: parseInt(process.env.API_PORT ?? '4010', 10),
db: {
host: process.env.DB_HOST ?? 'postgres',
port: parseInt(process.env.DB_PORT ?? '5432', 10),
name: process.env.DB_NAME ?? 'tripz',
user: process.env.DB_USER ?? 'tripz',
password: process.env.DB_PASSWORD ?? 'change_me_strong',
// بادئة الجداول لعزل Tripz عن باقي البرامج على نفس السيرفر
tablePrefix: process.env.DB_TABLE_PREFIX ?? 'tripz_',
synchronize: process.env.DB_SYNC === 'true',
},
redis: {
host: process.env.REDIS_HOST ?? 'redis',
port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
// DB رقم غير الافتراضي 0 لعزل Tripz عن باقي البرامج
db: parseInt(process.env.REDIS_DB ?? '3', 10),
keyPrefix: process.env.REDIS_KEY_PREFIX ?? 'tripz:',
},
queue: {
prefix: process.env.QUEUE_PREFIX ?? 'tripz_',
},
jwt: {
secret: process.env.JWT_SECRET ?? 'change_me_jwt_secret',
expires: process.env.JWT_EXPIRES ?? '15m',
refreshExpires: process.env.JWT_REFRESH_EXPIRES ?? '30d',
},
maps: {
tilesUrl: process.env.MAPS_TILES_URL ?? 'http://martin:3000',
provider: process.env.MAPS_PROVIDER ?? 'antlaq',
},
});
+22
View File
@@ -0,0 +1,22 @@
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { config as loadEnv } from 'dotenv';
loadEnv();
/**
* مصدر بيانات TypeORM — يُستخدم للهجرات و runtime.
* entityPrefix يضمن أن كل جداول Tripz تبدأ بـ tripz_ لعزلها على السيرفر المشترك.
*/
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST ?? 'postgres',
port: parseInt(process.env.DB_PORT ?? '5432', 10),
database: process.env.DB_NAME ?? 'tripz',
username: process.env.DB_USER ?? 'tripz',
password: process.env.DB_PASSWORD ?? 'change_me_strong',
entityPrefix: process.env.DB_TABLE_PREFIX ?? 'tripz_',
synchronize: false,
entities: [__dirname + '/../database/entities/*.entity.{ts,js}'],
migrations: [__dirname + '/../database/migrations/*.{ts,js}'],
});
@@ -0,0 +1,50 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
export type TenantMode = 'shared' | 'sovereign';
export type TenantPlan = 'launch' | 'brand' | 'fleet' | 'sovereign';
/**
* المستأجر = مكتب تكسي / أسطول / مشغّل. الجدول الفعلي: tripz_tenants
* (البادئة من entityPrefix). راجع docs/06 و docs/08.
*/
@Entity('tenants')
export class Tenant {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ unique: true })
slug: string;
@Column({ name: 'country_pack', default: 'jo' })
countryPack: string;
@Column({ type: 'varchar', default: 'launch' })
plan: TenantPlan;
@Column({ type: 'varchar', default: 'shared' })
mode: TenantMode;
@Column({ type: 'jsonb', default: {} })
branding: Record<string, any>;
@Column({ type: 'jsonb', default: {} })
features: Record<string, any>;
@Column({ default: 'active' })
status: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* أول هجرة: جدول المستأجرين + تفعيل PostGIS.
* اسم الجدول الفعلي tripz_tenants (البادئة تُضاف عبر entityPrefix، لكن الهجرة
* تكتب الاسم صراحةً لأنها SQL خام).
*/
export class InitTenants1721145600000 implements MigrationInterface {
public async up(q: QueryRunner): Promise<void> {
await q.query(`CREATE EXTENSION IF NOT EXISTS postgis`);
await q.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_tenants (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
name varchar NOT NULL,
slug varchar NOT NULL UNIQUE,
country_pack varchar NOT NULL DEFAULT 'jo',
plan varchar NOT NULL DEFAULT 'launch',
mode varchar NOT NULL DEFAULT 'shared',
branding jsonb NOT NULL DEFAULT '{}',
features jsonb NOT NULL DEFAULT '{}',
status varchar NOT NULL DEFAULT 'active',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
)
`);
}
public async down(q: QueryRunner): Promise<void> {
await q.query(`DROP TABLE IF EXISTS tripz_tenants`);
}
}
+30
View File
@@ -0,0 +1,30 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const cfg = app.get(ConfigService);
app.setGlobalPrefix('api');
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, transform: true }),
);
app.enableCors();
const swagger = new DocumentBuilder()
.setTitle('Tripz API')
.setDescription('منصة نقل ذكي متعددة المستأجرين')
.setVersion('0.1.0')
.addBearerAuth()
.build();
SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, swagger));
const port = cfg.get<number>('apiPort') ?? 4010;
await app.listen(port, '0.0.0.0');
Logger.log(`Tripz API on :${port} (docs at /api/docs)`, 'Bootstrap');
}
bootstrap();
@@ -0,0 +1,15 @@
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('health')
@Controller('health')
export class HealthController {
@Get()
check() {
return {
status: 'ok',
service: 'tripz-api',
time: new Date().toISOString(),
};
}
}
@@ -0,0 +1,5 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({ controllers: [HealthController] })
export class HealthModule {}
@@ -0,0 +1,27 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { TenantsService } from './tenants.service';
import { Tenant } from '../../database/entities/tenant.entity';
@ApiTags('tenants')
@Controller()
export class TenantsController {
constructor(private readonly tenants: TenantsService) {}
// للتطبيق: إعداد المستأجر الحالي عند الإقلاع.
@Get('tenant/config/:slug')
config(@Param('slug') slug: string) {
return this.tenants.config(slug);
}
// للسوبر-آدمن: إدارة كل المستأجرين (يُحمى بحارس دور لاحقاً).
@Get('admin/tenants')
list() {
return this.tenants.findAll();
}
@Post('admin/tenants')
create(@Body() body: Partial<Tenant>) {
return this.tenants.create(body);
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Tenant } from '../../database/entities/tenant.entity';
import { TenantsService } from './tenants.service';
import { TenantsController } from './tenants.controller';
@Module({
imports: [TypeOrmModule.forFeature([Tenant])],
controllers: [TenantsController],
providers: [TenantsService],
exports: [TenantsService],
})
export class TenantsModule {}
@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tenant } from '../../database/entities/tenant.entity';
@Injectable()
export class TenantsService {
constructor(
@InjectRepository(Tenant)
private readonly repo: Repository<Tenant>,
) {}
findAll(): Promise<Tenant[]> {
return this.repo.find();
}
findBySlug(slug: string): Promise<Tenant | null> {
return this.repo.findOne({ where: { slug } });
}
create(data: Partial<Tenant>): Promise<Tenant> {
return this.repo.save(this.repo.create(data));
}
/**
* الإعداد الديناميكي الذي يجلبه تطبيق فلاتر عند الإقلاع (GET /tenant/config).
* كل ما يمكن جعله ديناميكياً (نصوص، ميزات، ألوان، دفع) يأتي من هنا — راجع docs/06.
*/
async config(slug: string) {
const t = await this.findBySlug(slug);
if (!t) return null;
return {
slug: t.slug,
name: t.name,
countryPack: t.countryPack,
plan: t.plan,
branding: t.branding,
features: t.features,
};
}
}
+17
View File
@@ -0,0 +1,17 @@
import 'reflect-metadata';
import { Logger } from '@nestjs/common';
/**
* نقطة دخول الـ worker (BullMQ) — مهام غير متزامنة: انتهاء صلاحية العروض،
* الإشعارات، التسويات، تجميع usage للفوترة (راجع docs/09).
* حالياً هيكل فقط؛ المعالِجات تُضاف في P1.
*/
async function bootstrap() {
const log = new Logger('Worker');
const prefix = process.env.QUEUE_PREFIX ?? 'tripz_';
const redisDb = process.env.REDIS_DB ?? '3';
log.log(`Tripz worker up. queuePrefix=${prefix} redisDb=${redisDb}`);
// TODO(P1): سجّل معالِجات BullMQ هنا.
setInterval(() => void 0, 1 << 30);
}
bootstrap();
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false,
"esModuleInterop": true,
"resolveJsonModule": true
}
}