feat: establish backend foundation
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
DATABASE_URL="file:./prisma/dev.db"
|
||||||
|
FRONTEND_ORIGIN="http://localhost:5173"
|
||||||
|
PORT=3000
|
||||||
+7
-1
@@ -3,6 +3,9 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
# generated by `npx prisma generate`
|
||||||
|
/generated
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
@@ -20,7 +23,7 @@ lerna-debug.log*
|
|||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
.idea
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
.c9/
|
.c9/
|
||||||
@@ -54,3 +57,6 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# SQLite data
|
||||||
|
prisma/*.db
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Backend
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
NestJS API WealthLedger. Все функциональные изменения планируются в `../specs/openspec` до реализации.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Границы
|
||||||
|
|
||||||
|
- Публичный HTTP API версионируется как `/api/v1`; OpenAPI из backend - источник контракта для frontend.
|
||||||
|
- Backend отвечает за доменную логику, данные, конфигурацию и внешние интеграции. Frontend не вызывает T-Bank напрямую; будущая интеграция остается за backend boundary.
|
||||||
|
- Не размещать frontend-код, сгенерированный клиент или постоянную документацию в этом репозитории.
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint';
|
|||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: ['eslint.config.mjs'],
|
ignores: ['eslint.config.mjs', 'dist/', 'generated/'],
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
|||||||
Generated
+1947
-373
File diff suppressed because it is too large
Load Diff
+17
-4
@@ -6,23 +6,30 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "prisma generate && nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "NODE_ENV=development nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/src/main.js",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.4",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "11.4.5",
|
||||||
|
"@prisma/adapter-better-sqlite3": "^7.9.1",
|
||||||
|
"@prisma/client": "^7.9.1",
|
||||||
|
"better-sqlite3": "^12.6.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.15.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
@@ -42,6 +49,7 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"prisma": "^7.9.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
@@ -51,6 +59,11 @@
|
|||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@nestjs/swagger@11.4.5": {
|
||||||
|
"js-yaml": "5.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"js",
|
"js",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { defineConfig } from 'prisma/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: 'prisma/schema.prisma',
|
||||||
|
datasource: {
|
||||||
|
url: process.env['DATABASE_URL'],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../generated/prisma"
|
||||||
|
moduleFormat = "cjs"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
@@ -14,6 +16,41 @@ describe('AppController', () => {
|
|||||||
appController = app.get<AppController>(AppController);
|
appController = app.get<AppController>(AppController);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('documents the safe internal error response', async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
const app: INestApplication = module.createNestApplication();
|
||||||
|
app.setGlobalPrefix('api/v1');
|
||||||
|
|
||||||
|
const document: OpenAPIObject = SwaggerModule.createDocument(
|
||||||
|
app,
|
||||||
|
new DocumentBuilder().build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(document.paths['/api/v1']?.get?.responses['500']).toEqual({
|
||||||
|
description: 'Internal server error',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/SafeErrorResponseDto' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.paths['/api/v1']?.get?.responses['200']).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { type: 'string', example: 'Hello World!' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
describe('root', () => {
|
describe('root', () => {
|
||||||
it('should return "Hello World!"', () => {
|
it('should return "Hello World!"', () => {
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ApiOkResponse } from '@nestjs/swagger';
|
||||||
|
import { ApiSafeErrorResponse } from './common/decorators/api-safe-error-response.decorator';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
@@ -6,6 +8,10 @@ export class AppController {
|
|||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOkResponse({
|
||||||
|
schema: { type: 'string', example: 'Hello World!' },
|
||||||
|
})
|
||||||
|
@ApiSafeErrorResponse()
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -1,9 +1,18 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { validateEnv } from './config/env.validation';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
validate: validateEnv,
|
||||||
|
}),
|
||||||
|
PrismaModule,
|
||||||
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApiInternalServerErrorResponse, ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class SafeErrorResponseDto {
|
||||||
|
@ApiProperty({ example: 500 })
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Internal server error' })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '/api/v1' })
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiSafeErrorResponse(): MethodDecorator & ClassDecorator {
|
||||||
|
return ApiInternalServerErrorResponse({
|
||||||
|
description: 'Internal server error',
|
||||||
|
type: SafeErrorResponseDto,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ArgumentsHost } from '@nestjs/common';
|
||||||
|
import { HttpExceptionFilter } from './http-exception.filter';
|
||||||
|
|
||||||
|
describe('HttpExceptionFilter', () => {
|
||||||
|
it('does not expose internal error details', () => {
|
||||||
|
const status = jest.fn().mockReturnThis();
|
||||||
|
const json = jest.fn();
|
||||||
|
const filter = new HttpExceptionFilter();
|
||||||
|
const host = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
path: '/api/v1/health',
|
||||||
|
url: '/api/v1/health?access_token=secret',
|
||||||
|
}),
|
||||||
|
getResponse: () => ({ status, json }),
|
||||||
|
}),
|
||||||
|
} as unknown as ArgumentsHost;
|
||||||
|
|
||||||
|
filter.catch(new Error('database password: secret'), host);
|
||||||
|
|
||||||
|
expect(status).toHaveBeenCalledWith(500);
|
||||||
|
expect(json).toHaveBeenCalledWith({
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Internal server error',
|
||||||
|
path: '/api/v1/health',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
ArgumentsHost,
|
||||||
|
Catch,
|
||||||
|
ExceptionFilter,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost): void {
|
||||||
|
const context = host.switchToHttp();
|
||||||
|
const response = context.getResponse<Response>();
|
||||||
|
const request = context.getRequest<Request>();
|
||||||
|
const status =
|
||||||
|
exception instanceof HttpException
|
||||||
|
? exception.getStatus()
|
||||||
|
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
statusCode: status,
|
||||||
|
message: status === 500 ? 'Internal server error' : 'Request failed',
|
||||||
|
path: request.path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { validateEnv } from './env.validation';
|
||||||
|
|
||||||
|
describe('validateEnv', () => {
|
||||||
|
it('normalizes FRONTEND_ORIGIN to its canonical origin', () => {
|
||||||
|
expect(
|
||||||
|
validateEnv({
|
||||||
|
DATABASE_URL: 'file:./data.db',
|
||||||
|
FRONTEND_ORIGIN: 'http://localhost:5173/',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
DATABASE_URL: 'file:./data.db',
|
||||||
|
FRONTEND_ORIGIN: 'http://localhost:5173',
|
||||||
|
PORT: 3000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails fast when required configuration is missing', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateEnv({ FRONTEND_ORIGIN: 'http://localhost:5173' }),
|
||||||
|
).toThrow('DATABASE_URL must be a SQLite file URL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
'https://frontend.example/app',
|
||||||
|
'https://frontend.example?token=secret',
|
||||||
|
'https://frontend.example#fragment',
|
||||||
|
'ftp://frontend.example',
|
||||||
|
])(
|
||||||
|
'rejects a frontend origin that is not a canonical HTTP origin: %s',
|
||||||
|
(origin) => {
|
||||||
|
expect(() =>
|
||||||
|
validateEnv({
|
||||||
|
DATABASE_URL: 'file:./data.db',
|
||||||
|
FRONTEND_ORIGIN: origin,
|
||||||
|
}),
|
||||||
|
).toThrow('FRONTEND_ORIGIN must be a valid HTTP origin');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export interface Environment {
|
||||||
|
DATABASE_URL: string;
|
||||||
|
FRONTEND_ORIGIN: string;
|
||||||
|
PORT: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEnv(config: Record<string, unknown>): Environment {
|
||||||
|
const databaseUrl = config['DATABASE_URL'];
|
||||||
|
const frontendOrigin = config['FRONTEND_ORIGIN'];
|
||||||
|
const port = config['PORT'] ?? 3000;
|
||||||
|
|
||||||
|
if (typeof databaseUrl !== 'string' || !databaseUrl.startsWith('file:')) {
|
||||||
|
throw new Error('DATABASE_URL must be a SQLite file URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof frontendOrigin !== 'string') {
|
||||||
|
throw new Error('FRONTEND_ORIGIN must be a valid HTTP origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedOrigin: URL;
|
||||||
|
try {
|
||||||
|
parsedOrigin = new URL(frontendOrigin);
|
||||||
|
} catch {
|
||||||
|
throw new Error('FRONTEND_ORIGIN must be a valid HTTP origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(parsedOrigin.protocol !== 'http:' && parsedOrigin.protocol !== 'https:') ||
|
||||||
|
parsedOrigin.pathname !== '/' ||
|
||||||
|
parsedOrigin.search ||
|
||||||
|
parsedOrigin.hash
|
||||||
|
) {
|
||||||
|
throw new Error('FRONTEND_ORIGIN must be a valid HTTP origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPort = Number(port);
|
||||||
|
if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
||||||
|
throw new Error('PORT must be a valid port number');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
DATABASE_URL: databaseUrl,
|
||||||
|
FRONTEND_ORIGIN: parsedOrigin.origin,
|
||||||
|
PORT: parsedPort,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function shouldExposeSwagger(nodeEnv: string | undefined): boolean {
|
||||||
|
return nodeEnv === 'development';
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { shouldExposeSwagger } from './config/swagger.config';
|
||||||
|
|
||||||
|
describe('shouldExposeSwagger', () => {
|
||||||
|
it('does not expose Swagger outside development', () => {
|
||||||
|
expect(shouldExposeSwagger('test')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
+25
-2
@@ -1,8 +1,31 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
|
import { Environment } from './config/env.validation';
|
||||||
|
import { shouldExposeSwagger } from './config/swagger.config';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
const config = app.get<ConfigService<Environment, true>>(ConfigService);
|
||||||
|
const frontendOrigin = config.getOrThrow<string>('FRONTEND_ORIGIN');
|
||||||
|
|
||||||
|
app.enableCors({ origin: frontendOrigin });
|
||||||
|
app.setGlobalPrefix('api/v1');
|
||||||
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
|
||||||
|
if (shouldExposeSwagger(process.env['NODE_ENV'])) {
|
||||||
|
const document = SwaggerModule.createDocument(
|
||||||
|
app,
|
||||||
|
new DocumentBuilder()
|
||||||
|
.setTitle('WealthLedger API')
|
||||||
|
.setVersion('v1')
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
SwaggerModule.setup('api/docs', app, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.listen(config.getOrThrow('PORT'));
|
||||||
}
|
}
|
||||||
bootstrap();
|
void bootstrap();
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
|
||||||
|
import { PrismaClient } from '../../generated/prisma/client';
|
||||||
|
import { Environment } from '../config/env.validation';
|
||||||
|
|
||||||
|
function getDatabaseUrl(config: ConfigService<Environment, true>): string {
|
||||||
|
return config.getOrThrow<string>('DATABASE_URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService
|
||||||
|
extends PrismaClient
|
||||||
|
implements OnModuleInit, OnModuleDestroy
|
||||||
|
{
|
||||||
|
constructor(config: ConfigService<Environment, true>) {
|
||||||
|
super({
|
||||||
|
adapter: new PrismaBetterSqlite3({
|
||||||
|
url: getDatabaseUrl(config),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit(): Promise<void> {
|
||||||
|
return this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleDestroy(): Promise<void> {
|
||||||
|
return this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
process.env.DATABASE_URL = 'file:/tmp/wealth-ledger-e2e.db';
|
||||||
|
process.env.FRONTEND_ORIGIN = 'http://localhost:5173';
|
||||||
@@ -2,8 +2,12 @@
|
|||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
|
"setupFiles": ["<rootDir>/e2e-env.cjs"],
|
||||||
"testRegex": ".e2e-spec.ts$",
|
"testRegex": ".e2e-spec.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { existsSync, readFileSync } = require('node:fs');
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
|
||||||
|
|
||||||
|
assert.equal(packageJson.scripts['start:prod'], 'node dist/src/main.js');
|
||||||
|
assert.ok(existsSync('dist/src/main.js'));
|
||||||
|
assert.doesNotThrow(() => require('../dist/generated/prisma/client.js'));
|
||||||
Reference in New Issue
Block a user