Răsfoiți Sursa

feat: initialize box-app-api with configuration, health check, and Redis cache integration

Dave 2 luni în urmă
părinte
comite
5198b4117b

+ 26 - 0
.env.app.dev

@@ -0,0 +1,26 @@
+# 测试服环境变量
+APP_ENV=development
+
+# Prisma Config
+# Dave local
+MONGO_URL="mongodb://admin:ZXcv%21%21996@localhost:27017/box_admin?authSource=admin"
+
+# office dev env
+# MONGO_URL="mongodb://msAdmin:Fl1%2A29MJe%26jLvj@localhost:27017/box_admin?authSource=admin"
+
+# Redis Config
+REDIS_HOST=127.0.0.1
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=0
+REDIS_KEY_PREFIX=box:
+
+# App set to 0.0.0.0 for local LAN access
+APP_HOST=0.0.0.0
+APP_PORT=3301
+APP_CORS_ORIGIN=*
+
+
+# JWT
+JWT_SECRET=047df8aaa3d17dc1173c5a9a3052ba66c2b0bd96937147eb643319a0c90d132f
+JWT_ACCESS_TOKEN_TTL=43200

+ 8 - 0
apps/box-app-api/nest-cli.json

@@ -0,0 +1,8 @@
+{
+  "box-app-api": {
+    "type": "application",
+    "root": "apps/box-app-api",
+    "entryFile": "main",
+    "sourceRoot": "apps/box-app-api/src"
+  }
+}

+ 10 - 0
apps/box-app-api/package.json

@@ -0,0 +1,10 @@
+{
+  "name": "box-app-api",
+  "version": "1.0.0",
+  "private": true,
+  "main": "dist/main.js",
+  "scripts": {
+    "start": "nest start",
+    "start:dev": "nest start --watch"
+  }
+}

+ 0 - 0
apps/box-app-api/project.json


+ 27 - 0
apps/box-app-api/src/app.module.ts

@@ -0,0 +1,27 @@
+import { Module } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
+
+import { RedisCacheModule } from './redis/redis-cache.module';
+import { HealthModule } from './health/health.module';
+import { PrismaMongoModule } from './prisma/prisma-mongo.module';
+
+@Module({
+  imports: [
+    // Global config, reuse .env + .env.app.dev at repo root
+    ConfigModule.forRoot({
+      isGlobal: true,
+      envFilePath: ['.env.app.dev', '.env'],
+      expandVariables: true,
+    }),
+
+    // Global Redis cache
+    RedisCacheModule,
+
+    // Mongo Prisma client for box_app DB (stub, to wire real client later)
+    PrismaMongoModule,
+
+    // Simple health endpoint
+    HealthModule,
+  ],
+})
+export class AppModule {}

+ 15 - 0
apps/box-app-api/src/health/health.controller.ts

@@ -0,0 +1,15 @@
+import { Controller, Get } from '@nestjs/common';
+
+@Controller('health')
+export class HealthController {
+  @Get()
+  getHealth() {
+    // For DB backed checks, later you can also ping Mongo / Redis here.
+    return {
+      status: 'ok',
+      service: 'box-app-api',
+      // Use number in JSON; use BigInt only at DB level, as per your rule.
+      ts: Date.now(),
+    };
+  }
+}

+ 7 - 0
apps/box-app-api/src/health/health.module.ts

@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common';
+import { HealthController } from './health.controller';
+
+@Module({
+  controllers: [HealthController],
+})
+export class HealthModule {}

+ 73 - 0
apps/box-app-api/src/main.ts

@@ -0,0 +1,73 @@
+import { NestFactory } from '@nestjs/core';
+import { Logger, ValidationPipe } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import helmet from 'helmet';
+import compression from 'compression';
+
+import { AppModule } from './app.module';
+
+async function bootstrap() {
+  const app = await NestFactory.create(AppModule, {
+    bufferLogs: true,
+  });
+
+  const logger = new Logger('Bootstrap');
+  app.useLogger(logger);
+
+  const configService = app.get(ConfigService);
+
+  const host =
+    configService.get<string>('APP_HOST') ??
+    configService.get<string>('HOST') ??
+    '0.0.0.0';
+
+  const port =
+    configService.get<number>('APP_PORT') ?? Number(process.env.PORT ?? 3301);
+
+  const crossOrigin =
+    configService.get<string>('APP_CROSS_ORIGIN') ??
+    configService.get<string>('CROSS_ORIGIN') ??
+    '*';
+
+  app.enableCors({
+    origin:
+      crossOrigin === '*' ? true : crossOrigin.split(',').map((o) => o.trim()),
+    methods: 'GET,PUT,PATCH,POST,DELETE',
+  });
+
+  // 👇 Important: this makes /health become /api/v1/health
+  app.setGlobalPrefix('api/v1', {
+    exclude: ['/'],
+  });
+
+  app.use(helmet());
+  app.use(compression());
+
+  app.enableCors({
+    origin: true,
+    credentials: true,
+  });
+
+  app.useGlobalPipes(
+    new ValidationPipe({
+      whitelist: true,
+      transform: true,
+      transformOptions: {
+        enableImplicitConversion: true,
+      },
+      forbidNonWhitelisted: false,
+    }),
+  );
+
+  await app.listen(port, host);
+
+  const url = `http://${host}:${port}`;
+
+  logger.log(`🚀 box-app-api listening on ${url} (global prefix: /api/v1)`);
+}
+
+bootstrap().catch((error) => {
+  // eslint-disable-next-line no-console
+  console.error('❌ Failed to bootstrap box-app-api', error);
+  process.exit(1);
+});

+ 9 - 0
apps/box-app-api/src/prisma/prisma-mongo.module.ts

@@ -0,0 +1,9 @@
+import { Global, Module } from '@nestjs/common';
+import { PrismaMongoService } from './prisma-mongo.service';
+
+@Global()
+@Module({
+  providers: [PrismaMongoService],
+  exports: [PrismaMongoService],
+})
+export class PrismaMongoModule {}

+ 32 - 0
apps/box-app-api/src/prisma/prisma-mongo.service.ts

@@ -0,0 +1,32 @@
+import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
+
+// NOTE:
+// Replace this import with your real generated Mongo client.
+// Example (depending on your prisma mongo generator):
+// import { PrismaClient as MongoPrismaClient } from '@prisma/client-mongo';
+
+class MongoPrismaClientMock {
+  // Temporary mock client so the app can boot without the real client.
+  // Replace this with the actual Prisma client usage.
+  async $connect(): Promise<void> {
+    return;
+  }
+
+  async $disconnect(): Promise<void> {
+    return;
+  }
+}
+
+@Injectable()
+export class PrismaMongoService
+  extends MongoPrismaClientMock
+  implements OnModuleInit, OnModuleDestroy
+{
+  async onModuleInit(): Promise<void> {
+    await this.$connect();
+  }
+
+  async onModuleDestroy(): Promise<void> {
+    await this.$disconnect();
+  }
+}

+ 35 - 0
apps/box-app-api/src/redis/redis-cache.module.ts

@@ -0,0 +1,35 @@
+import { CacheModule } from '@nestjs/cache-manager';
+import type { CacheModuleOptions } from '@nestjs/cache-manager';
+import { Module } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { redisStore } from 'cache-manager-ioredis-yet';
+
+@Module({
+  imports: [
+    CacheModule.registerAsync<CacheModuleOptions>({
+      isGlobal: true,
+      inject: [ConfigService],
+      useFactory: async (configService: ConfigService) => {
+        const host = configService.get<string>('REDIS_HOST') ?? '127.0.0.1';
+        const port = configService.get<number>('REDIS_PORT') ?? 6379;
+        const password = configService.get<string | undefined>(
+          'REDIS_PASSWORD',
+        );
+
+        const store = await redisStore({
+          host,
+          port,
+          password,
+        });
+
+        return {
+          store,
+          // default TTL in seconds for cache-manager v5 (Nest’s current rec):
+          ttl: 10,
+        };
+      },
+    }),
+  ],
+  exports: [CacheModule],
+})
+export class RedisCacheModule {}

+ 4 - 0
apps/box-app-api/tsconfig.build.json

@@ -0,0 +1,4 @@
+{
+  "extends": "./tsconfig.json",
+  "exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
+}

+ 7 - 0
apps/box-app-api/tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/apps/box-app-api"
+  },
+  "include": ["src/**/*.ts"]
+}

+ 9 - 0
nest-cli.json

@@ -14,6 +14,15 @@
       "compilerOptions": {
         "tsConfigPath": "apps/box-mgnt-api/tsconfig.json"
       }
+    },
+    "box-app-api": {
+      "type": "application",
+      "root": "apps/box-app-api",
+      "entryFile": "main",
+      "sourceRoot": "apps/box-app-api/src",
+      "compilerOptions": {
+        "tsConfigPath": "apps/box-app-api/tsconfig.json"
+      }
     }
   }
 }

+ 5 - 1
package.json

@@ -6,6 +6,7 @@
     "dev:mgnt": "nest start box-mgnt-api --watch",
     "build:mgnt": "nest build box-mgnt-api",
     "start:mgnt": "node dist/apps/box-mgnt-api/main.js",
+    "dev:app": "dotenv -e .env.app.dev -- nest start box-app-api --watch",
     "prisma:migrate:dev:mysql": "dotenv -e .env.mgnt.dev -- prisma migrate dev --schema=prisma/mysql/schema",
     "prisma:migrate:reset:mysql": "dotenv -e .env.mgnt.dev -- prisma migrate reset --schema=prisma/mysql/schema",
     "prisma:generate:mysql": "dotenv -e .env.mgnt.dev -- prisma generate --schema=prisma/mysql/schema",
@@ -26,7 +27,7 @@
     "@fastify/multipart": "^7.7.3",
     "@fastify/static": "^6.12.0",
     "@nestjs/axios": "^3.0.2",
-    "@nestjs/cache-manager": "^2.2.2",
+    "@nestjs/cache-manager": "^2.3.0",
     "@nestjs/common": "^10.3.8",
     "@nestjs/config": "^3.3.0",
     "@nestjs/core": "^10.3.8",
@@ -44,13 +45,16 @@
     "bowser": "^2.11.0",
     "bson-objectid": "^2.0.4",
     "cache-manager": "^5.5.2",
+    "cache-manager-ioredis-yet": "^2.1.2",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.1",
     "class-validator-extended": "^4.0.0",
+    "compression": "^1.8.1",
     "dayjs": "^1.11.11",
     "dotenv": "^16.5.0",
     "exceljs": "^4.4.0",
     "fastify": "^4.26.2",
+    "helmet": "^8.1.0",
     "ioredis": "^5.8.2",
     "lib-qqwry": "^1.3.4",
     "lodash": "^4.17.21",

+ 86 - 1
pnpm-lock.yaml

@@ -27,7 +27,7 @@ importers:
         specifier: ^3.0.2
         version: 3.1.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)
       '@nestjs/cache-manager':
-        specifier: ^2.2.2
+        specifier: ^2.3.0
         version: 2.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(cache-manager@5.7.6)(rxjs@7.8.2)
       '@nestjs/common':
         specifier: ^10.3.8
@@ -80,6 +80,9 @@ importers:
       cache-manager:
         specifier: ^5.5.2
         version: 5.7.6
+      cache-manager-ioredis-yet:
+        specifier: ^2.1.2
+        version: 2.1.2
       class-transformer:
         specifier: ^0.5.1
         version: 0.5.1
@@ -89,6 +92,9 @@ importers:
       class-validator-extended:
         specifier: ^4.0.0
         version: 4.2.1(class-validator@0.14.2)(validator@13.15.23)
+      compression:
+        specifier: ^1.8.1
+        version: 1.8.1
       dayjs:
         specifier: ^1.11.11
         version: 1.11.19
@@ -101,6 +107,9 @@ importers:
       fastify:
         specifier: ^4.26.2
         version: 4.29.1
+      helmet:
+        specifier: ^8.1.0
+        version: 8.1.0
       ioredis:
         specifier: ^5.8.2
         version: 5.8.2
@@ -265,6 +274,8 @@ importers:
         specifier: ^5.4.5
         version: 5.9.3
 
+  apps/box-app-api: {}
+
   libs/common: {}
 
   libs/core: {}
@@ -2392,6 +2403,11 @@ packages:
     resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
     engines: {node: '>= 0.8'}
 
+  cache-manager-ioredis-yet@2.1.2:
+    resolution: {integrity: sha512-p/5D+ADvJaZjAs12fR5l0ZJ+rK2EqbCryFdrzsMj3K+lGwNoCjB33N6V397otgreB+iwK+lssBshpkJDodiyMQ==}
+    engines: {node: '>= 18'}
+    deprecated: With cache-manager v6 we now are using Keyv
+
   cache-manager@5.7.6:
     resolution: {integrity: sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==}
     engines: {node: '>= 18'}
@@ -2583,6 +2599,14 @@ packages:
     resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==}
     engines: {node: '>= 10'}
 
+  compressible@2.0.18:
+    resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
+    engines: {node: '>= 0.6'}
+
+  compression@1.8.1:
+    resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
+    engines: {node: '>= 0.8.0'}
+
   concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
@@ -3325,6 +3349,10 @@ packages:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
 
+  helmet@8.1.0:
+    resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
+    engines: {node: '>=18.0.0'}
+
   help-me@5.0.0:
     resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
 
@@ -3867,6 +3895,9 @@ packages:
   makeerror@1.0.12:
     resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
 
+  map-or-similar@1.5.0:
+    resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
+
   math-intrinsics@1.1.0:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
@@ -3879,6 +3910,9 @@ packages:
     resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
     engines: {node: '>= 4.0.0'}
 
+  memoizerific@1.11.3:
+    resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
+
   memory-pager@1.5.0:
     resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
 
@@ -4040,6 +4074,10 @@ packages:
     resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
     engines: {node: '>= 0.6'}
 
+  negotiator@0.6.4:
+    resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
+    engines: {node: '>= 0.6'}
+
   neo-async@2.6.2:
     resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
 
@@ -4120,6 +4158,10 @@ packages:
     resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
     engines: {node: '>= 0.8'}
 
+  on-headers@1.1.0:
+    resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
+    engines: {node: '>= 0.8'}
+
   once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
 
@@ -4834,6 +4876,9 @@ packages:
     resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
     engines: {node: '>=10'}
 
+  telejson@7.2.0:
+    resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==}
+
   terser-webpack-plugin@5.3.14:
     resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==}
     engines: {node: '>= 10.13.0'}
@@ -8079,6 +8124,14 @@ snapshots:
 
   bytes@3.1.2: {}
 
+  cache-manager-ioredis-yet@2.1.2:
+    dependencies:
+      cache-manager: 5.7.6
+      ioredis: 5.8.2
+      telejson: 7.2.0
+    transitivePeerDependencies:
+      - supports-color
+
   cache-manager@5.7.6:
     dependencies:
       eventemitter3: 5.0.1
@@ -8269,6 +8322,22 @@ snapshots:
       normalize-path: 3.0.0
       readable-stream: 3.6.2
 
+  compressible@2.0.18:
+    dependencies:
+      mime-db: 1.54.0
+
+  compression@1.8.1:
+    dependencies:
+      bytes: 3.1.2
+      compressible: 2.0.18
+      debug: 2.6.9
+      negotiator: 0.6.4
+      on-headers: 1.1.0
+      safe-buffer: 5.2.1
+      vary: 1.1.2
+    transitivePeerDependencies:
+      - supports-color
+
   concat-map@0.0.1: {}
 
   concat-stream@2.0.0:
@@ -9136,6 +9205,8 @@ snapshots:
     dependencies:
       function-bind: 1.1.2
 
+  helmet@8.1.0: {}
+
   help-me@5.0.0: {}
 
   html-escaper@2.0.2: {}
@@ -9880,6 +9951,8 @@ snapshots:
     dependencies:
       tmpl: 1.0.5
 
+  map-or-similar@1.5.0: {}
+
   math-intrinsics@1.1.0: {}
 
   media-typer@0.3.0: {}
@@ -9888,6 +9961,10 @@ snapshots:
     dependencies:
       fs-monkey: 1.1.0
 
+  memoizerific@1.11.3:
+    dependencies:
+      map-or-similar: 1.5.0
+
   memory-pager@1.5.0: {}
 
   merge-descriptors@1.0.3: {}
@@ -9995,6 +10072,8 @@ snapshots:
 
   negotiator@0.6.3: {}
 
+  negotiator@0.6.4: {}
+
   neo-async@2.6.2: {}
 
   nestjs-pino@4.4.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@9.0.0)(pino@9.14.0)(rxjs@7.8.2):
@@ -10055,6 +10134,8 @@ snapshots:
     dependencies:
       ee-first: 1.1.1
 
+  on-headers@1.1.0: {}
+
   once@1.4.0:
     dependencies:
       wrappy: 1.0.2
@@ -10780,6 +10861,10 @@ snapshots:
       mkdirp: 1.0.4
       yallist: 4.0.0
 
+  telejson@7.2.0:
+    dependencies:
+      memoizerific: 1.11.3
+
   terser-webpack-plugin@5.3.14(@swc/core@1.15.2)(webpack@5.97.1(@swc/core@1.15.2)):
     dependencies:
       '@jridgewell/trace-mapping': 0.3.31