feat: Добавлена базовая структура приложения Home Service с бэкендом на NestJS и фронтендом на Next.js для управления событиями.
This commit is contained in:
98
backend/README.md
Normal file
98
backend/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ pnpm run start
|
||||
|
||||
# watch mode
|
||||
$ pnpm run start:dev
|
||||
|
||||
# production mode
|
||||
$ pnpm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ pnpm run test
|
||||
|
||||
# e2e tests
|
||||
$ pnpm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ pnpm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ pnpm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
107
backend/biome.json
Normal file
107
backend/biome.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"coverage",
|
||||
"migrations"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"noExtraBooleanCast": "error",
|
||||
"noMultipleSpacesInRegularExpressionLiterals": "error",
|
||||
"noUselessCatch": "error",
|
||||
"noUselessTypeConstraint": "error",
|
||||
"noWith": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noConstAssign": "error",
|
||||
"noConstantCondition": "error",
|
||||
"noEmptyCharacterClassInRegex": "error",
|
||||
"noEmptyPattern": "error",
|
||||
"noGlobalObjectCalls": "error",
|
||||
"noInvalidConstructorSuper": "error",
|
||||
"noInvalidNewBuiltin": "error",
|
||||
"noNonoctalDecimalEscape": "error",
|
||||
"noPrecisionLoss": "error",
|
||||
"noSelfAssign": "error",
|
||||
"noSetterReturn": "error",
|
||||
"noSwitchDeclarations": "error",
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnreachable": "error",
|
||||
"noUnreachableSuper": "error",
|
||||
"noUnsafeFinally": "error",
|
||||
"noUnsafeOptionalChaining": "error",
|
||||
"noUnusedLabels": "error",
|
||||
"noUnusedVariables": "warn",
|
||||
"useIsNan": "error",
|
||||
"useValidForDirection": "error",
|
||||
"useYield": "error"
|
||||
},
|
||||
"style": {
|
||||
"noArguments": "error",
|
||||
"noVar": "error",
|
||||
"useConst": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noAsyncPromiseExecutor": "error",
|
||||
"noCatchAssign": "error",
|
||||
"noClassAssign": "error",
|
||||
"noCompareNegZero": "error",
|
||||
"noControlCharactersInRegex": "error",
|
||||
"noDebugger": "error",
|
||||
"noDoubleEquals": "warn",
|
||||
"noDuplicateCase": "error",
|
||||
"noDuplicateClassMembers": "error",
|
||||
"noDuplicateObjectKeys": "error",
|
||||
"noDuplicateParameters": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noExplicitAny": "warn",
|
||||
"noExtraNonNullAssertion": "error",
|
||||
"noFallthroughSwitchClause": "error",
|
||||
"noFunctionAssign": "error",
|
||||
"noGlobalAssign": "error",
|
||||
"noImportAssign": "error",
|
||||
"noMisleadingCharacterClass": "error",
|
||||
"noPrototypeBuiltins": "error",
|
||||
"noRedeclare": "error",
|
||||
"noShadowRestrictedNames": "error",
|
||||
"noUnsafeNegation": "error",
|
||||
"useGetterReturn": "error",
|
||||
"useValidTypeof": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"jsxQuoteStyle": "single",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteProperties": "asNeeded"
|
||||
}
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
11
backend/drizzle.config.ts
Normal file
11
backend/drizzle.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/database/schema.ts',
|
||||
out: './migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
// PGLite will be initialized in code
|
||||
url: process.env.DATABASE_URL || 'postgresql://localhost/events',
|
||||
},
|
||||
});
|
||||
17
backend/migrations/0000_lyrical_gabe_jones.sql
Normal file
17
backend/migrations/0000_lyrical_gabe_jones.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE "events" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"emoji" text NOT NULL,
|
||||
"month" integer NOT NULL,
|
||||
"day" integer NOT NULL,
|
||||
"start_year" integer,
|
||||
"end_month" integer,
|
||||
"end_day" integer,
|
||||
"end_year" integer,
|
||||
"description" text,
|
||||
"color" text,
|
||||
"is_active" boolean DEFAULT true NOT NULL,
|
||||
"tags" text[],
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
126
backend/migrations/meta/0000_snapshot.json
Normal file
126
backend/migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "852b1a6a-31c7-4f44-bdef-a378a18e2c7d",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.events": {
|
||||
"name": "events",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"emoji": {
|
||||
"name": "emoji",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"month": {
|
||||
"name": "month",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"day": {
|
||||
"name": "day",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"start_year": {
|
||||
"name": "start_year",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"end_month": {
|
||||
"name": "end_month",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"end_day": {
|
||||
"name": "end_day",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"end_year": {
|
||||
"name": "end_year",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"tags": {
|
||||
"name": "tags",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
backend/migrations/meta/_journal.json
Normal file
13
backend/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1764421778123,
|
||||
"tag": "0000_lyrical_gabe_jones",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
8
backend/nest-cli.json
Normal file
8
backend/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
73
backend/package.json
Normal file
73
backend/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@home-service/backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "biome format --write ./src ./test",
|
||||
"lint": "biome lint --write ./src ./test",
|
||||
"check": "biome check --write ./src ./test",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electric-sql/pglite": "^0.3.14",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"jest": "^30.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
6433
backend/pnpm-lock.yaml
generated
Normal file
6433
backend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
backend/src/app.module.ts
Normal file
15
backend/src/app.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { EventsModule } from './events/events.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
DatabaseModule,
|
||||
EventsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
61
backend/src/database/database.module.ts
Normal file
61
backend/src/database/database.module.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { PGlite } from '@electric-sql/pglite';
|
||||
import { drizzle, PgliteDatabase } from 'drizzle-orm/pglite';
|
||||
import * as schema from './schema';
|
||||
import { join } from 'path';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
|
||||
|
||||
const databaseProvider = {
|
||||
provide: DATABASE_CONNECTION,
|
||||
useFactory: async (): Promise<PgliteDatabase<typeof schema>> => {
|
||||
const dbPath = process.env.DATABASE_PATH || './data/events.db';
|
||||
|
||||
// Убедимся, что директория существует
|
||||
const dbDir = dbPath.substring(0, dbPath.lastIndexOf('/'));
|
||||
if (dbDir && !existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const client = new PGlite(dbPath);
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
// Создаем таблицу напрямую вместо использования миграций
|
||||
try {
|
||||
await client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
emoji TEXT NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
day INTEGER NOT NULL,
|
||||
start_year INTEGER,
|
||||
end_month INTEGER,
|
||||
end_day INTEGER,
|
||||
end_year INTEGER,
|
||||
description TEXT,
|
||||
color TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
tags TEXT[],
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
console.log('✅ Database initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error initializing database:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return db;
|
||||
},
|
||||
};
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [databaseProvider],
|
||||
exports: [DATABASE_CONNECTION],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
39
backend/src/database/schema.ts
Normal file
39
backend/src/database/schema.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { pgTable, text, integer, boolean, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const events = pgTable('events', {
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
title: text('title').notNull(),
|
||||
emoji: text('emoji').notNull(),
|
||||
|
||||
// Дата начала события (обязательно для всех типов)
|
||||
month: integer('month').notNull(), // 1-12
|
||||
day: integer('day').notNull(), // 1-31
|
||||
|
||||
// Год начала (опционально)
|
||||
startYear: integer('start_year'),
|
||||
|
||||
// Дата окончания (опционально, для продолжительных событий)
|
||||
endMonth: integer('end_month'), // 1-12
|
||||
endDay: integer('end_day'), // 1-31
|
||||
endYear: integer('end_year'),
|
||||
|
||||
// Дополнительные поля
|
||||
description: text('description'),
|
||||
color: text('color'), // hex формат
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
tags: text('tags').array(),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
});
|
||||
|
||||
export type Event = typeof events.$inferSelect;
|
||||
export type NewEvent = typeof events.$inferInsert;
|
||||
60
backend/src/events/events.controller.ts
Normal file
60
backend/src/events/events.controller.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { EventsService } from './events.service';
|
||||
import type { CreateEventDto } from './schemas/event.schema';
|
||||
import { createEventSchema } from './schemas/event.schema';
|
||||
import { ZodValidationPipe } from '../pipes/zod-validation.pipe';
|
||||
|
||||
@Controller()
|
||||
export class EventsController {
|
||||
constructor(private readonly eventsService: EventsService) {}
|
||||
|
||||
/**
|
||||
* Главный endpoint для виджета Glance
|
||||
*/
|
||||
@Get('countdown')
|
||||
async getCountdown() {
|
||||
return this.eventsService.getCountdownData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все события
|
||||
*/
|
||||
@Get('api/events')
|
||||
async findAll() {
|
||||
return this.eventsService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить одно событие
|
||||
*/
|
||||
@Get('api/events/:id')
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.eventsService.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать событие
|
||||
*/
|
||||
@Post('api/events')
|
||||
@UsePipes(new ZodValidationPipe(createEventSchema))
|
||||
async create(@Body() createEventDto: CreateEventDto) {
|
||||
return this.eventsService.create(createEventDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить событие
|
||||
*/
|
||||
@Delete('api/events/:id')
|
||||
async remove(@Param('id') id: string) {
|
||||
await this.eventsService.remove(id);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
10
backend/src/events/events.module.ts
Normal file
10
backend/src/events/events.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EventsController } from './events.controller';
|
||||
import { EventsService } from './events.service';
|
||||
|
||||
@Module({
|
||||
controllers: [EventsController],
|
||||
providers: [EventsService],
|
||||
exports: [EventsService],
|
||||
})
|
||||
export class EventsModule {}
|
||||
200
backend/src/events/events.service.ts
Normal file
200
backend/src/events/events.service.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||
import { PgliteDatabase } from 'drizzle-orm/pglite';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { DATABASE_CONNECTION } from '../database/database.module';
|
||||
import { events, Event } from '../database/schema';
|
||||
import { CreateEventDto } from './schemas/event.schema';
|
||||
import {
|
||||
getEventType,
|
||||
getNextOccurrence,
|
||||
calculateTimeDiff,
|
||||
calculateAnniversary,
|
||||
calculateDuration,
|
||||
} from '../utils/datetime.utils';
|
||||
import { addMonths, getDaysInMonth } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class EventsService {
|
||||
constructor(
|
||||
@Inject(DATABASE_CONNECTION)
|
||||
private db: PgliteDatabase<typeof import('../database/schema')>,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<Event[]> {
|
||||
return this.db.select().from(events).where(eq(events.isActive, true));
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Event> {
|
||||
const [event] = await this.db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq(events.id, id));
|
||||
|
||||
if (!event) {
|
||||
throw new NotFoundException(`Event with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
async create(createEventDto: CreateEventDto): Promise<Event> {
|
||||
const [event] = await this.db
|
||||
.insert(events)
|
||||
.values(createEventDto)
|
||||
.returning();
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
async update(id: string, updateEventDto: Partial<CreateEventDto>): Promise<Event> {
|
||||
const [event] = await this.db
|
||||
.update(events)
|
||||
.set(updateEventDto)
|
||||
.where(eq(events.id, id))
|
||||
.returning();
|
||||
|
||||
if (!event) {
|
||||
throw new NotFoundException(`Event with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.db.delete(events).where(eq(events.id, id));
|
||||
// PGLite doesn't return rowCount, just assume it worked
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все события с расчетами для виджета countdown
|
||||
*/
|
||||
async getCountdownData() {
|
||||
const now = new Date();
|
||||
const allEvents = await this.findAll();
|
||||
|
||||
const result = [];
|
||||
|
||||
for (const event of allEvents) {
|
||||
const type = getEventType(event);
|
||||
const processedEvent: any = {
|
||||
id: event.id,
|
||||
type,
|
||||
emoji: event.emoji,
|
||||
name: event.title,
|
||||
description: event.description,
|
||||
color: event.color,
|
||||
tags: event.tags,
|
||||
};
|
||||
|
||||
if (type === 'recurring') {
|
||||
// Обычное повторяющееся событие
|
||||
const target = getNextOccurrence(now, event.month, event.day);
|
||||
const { display, days, hours, minutes } = calculateTimeDiff(target, now);
|
||||
|
||||
processedEvent.date = target.toISOString().split('T')[0];
|
||||
processedEvent.display = display;
|
||||
processedEvent.days = days;
|
||||
processedEvent.hours = hours;
|
||||
processedEvent.minutes = minutes;
|
||||
} else if (type === 'anniversary') {
|
||||
// Годовщина
|
||||
const { years, days } = calculateAnniversary(
|
||||
event.startYear!,
|
||||
event.month,
|
||||
event.day,
|
||||
now,
|
||||
);
|
||||
const nextTarget = getNextOccurrence(now, event.month, event.day);
|
||||
const { display, days: nextDays, hours, minutes } = calculateTimeDiff(nextTarget, now);
|
||||
|
||||
processedEvent.date = nextTarget.toISOString().split('T')[0];
|
||||
processedEvent.startYear = event.startYear;
|
||||
processedEvent.yearsPassed = years;
|
||||
processedEvent.daysPassed = days;
|
||||
processedEvent.display = display;
|
||||
processedEvent.days = nextDays;
|
||||
processedEvent.hours = hours;
|
||||
processedEvent.minutes = minutes;
|
||||
} else if (type === 'duration') {
|
||||
// Продолжительное событие
|
||||
const durationInfo = calculateDuration(
|
||||
event.month,
|
||||
event.day,
|
||||
event.startYear!,
|
||||
event.endMonth!,
|
||||
event.endDay!,
|
||||
event.endYear!,
|
||||
now,
|
||||
);
|
||||
|
||||
processedEvent.startDate = new Date(event.startYear!, event.month - 1, event.day)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
processedEvent.endDate = new Date(event.endYear!, event.endMonth! - 1, event.endDay!)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
processedEvent.isActive = durationInfo.isActive;
|
||||
processedEvent.daysUntilStart = durationInfo.daysUntilStart;
|
||||
processedEvent.daysRemaining = durationInfo.daysRemaining;
|
||||
processedEvent.totalDays = durationInfo.totalDays;
|
||||
}
|
||||
|
||||
result.push(processedEvent);
|
||||
}
|
||||
|
||||
// Генерируем метки месяцев (как в Python версии)
|
||||
const months = [];
|
||||
let daysAccumulated = getDaysInMonth(now) - now.getDate();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const targetDate = addMonths(now, i);
|
||||
const monthName = new Intl.DateTimeFormat('ru', { month: 'short' }).format(targetDate);
|
||||
|
||||
months.push({
|
||||
name: monthName,
|
||||
days: daysAccumulated,
|
||||
});
|
||||
|
||||
daysAccumulated += getDaysInMonth(targetDate);
|
||||
}
|
||||
|
||||
// Timeline grid (как в Python версии)
|
||||
const timelineGrid = [];
|
||||
|
||||
// Зона 1: 14 дней
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const targetDate = new Date(now);
|
||||
targetDate.setDate(now.getDate() + i);
|
||||
|
||||
timelineGrid.push({
|
||||
days: i,
|
||||
date: targetDate.toLocaleDateString('ru-RU'),
|
||||
weekday: new Intl.DateTimeFormat('ru', { weekday: 'short' }).format(targetDate),
|
||||
});
|
||||
}
|
||||
|
||||
// Зона 2: 40 недель
|
||||
for (let i = 1; i <= 40; i++) {
|
||||
const daysAhead = 14 + i * 7;
|
||||
const targetDate = new Date(now);
|
||||
targetDate.setDate(now.getDate() + daysAhead);
|
||||
|
||||
timelineGrid.push({
|
||||
days: daysAhead,
|
||||
date: targetDate.toLocaleDateString('ru-RU'),
|
||||
weekday: new Intl.DateTimeFormat('ru', { weekday: 'short' }).format(targetDate),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
events: result,
|
||||
current: {
|
||||
year: now.getFullYear(),
|
||||
month: now.getMonth() + 1,
|
||||
day: now.getDate(),
|
||||
},
|
||||
months,
|
||||
timelineGrid,
|
||||
};
|
||||
}
|
||||
}
|
||||
50
backend/src/events/schemas/event.schema.ts
Normal file
50
backend/src/events/schemas/event.schema.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Базовая схема события
|
||||
export const createEventSchema = z.object({
|
||||
title: z.string().min(1, 'Название обязательно'),
|
||||
emoji: z.string().min(1, 'Эмодзи обязательно'),
|
||||
month: z.number().int().min(1).max(12),
|
||||
day: z.number().int().min(1).max(31),
|
||||
startYear: z.number().int().min(1900).max(2100).optional().nullable(),
|
||||
endMonth: z.number().int().min(1).max(12).optional().nullable(),
|
||||
endDay: z.number().int().min(1).max(31).optional().nullable(),
|
||||
endYear: z.number().int().min(1900).max(2100).optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().nullable(),
|
||||
isActive: z.boolean().optional().default(true),
|
||||
tags: z.array(z.string()).optional().nullable(),
|
||||
});
|
||||
|
||||
export const updateEventSchema = createEventSchema.partial();
|
||||
|
||||
export const eventResponseSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: z.enum(['recurring', 'anniversary', 'duration']),
|
||||
title: z.string(),
|
||||
emoji: z.string(),
|
||||
month: z.number(),
|
||||
day: z.number(),
|
||||
startYear: z.number().nullable(),
|
||||
endMonth: z.number().nullable(),
|
||||
endDay: z.number().nullable(),
|
||||
endYear: z.number().nullable(),
|
||||
description: z.string().nullable(),
|
||||
color: z.string().nullable(),
|
||||
isActive: z.boolean(),
|
||||
tags: z.array(z.string()).nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
// Вычисленные поля
|
||||
date: z.string().optional(),
|
||||
display: z.string().optional(),
|
||||
days: z.number().optional(),
|
||||
hours: z.number().optional(),
|
||||
minutes: z.number().optional(),
|
||||
yearsПassed: z.number().optional(),
|
||||
daysPassed: z.number().optional(),
|
||||
});
|
||||
|
||||
export type CreateEventDto = z.infer<typeof createEventSchema>;
|
||||
export type UpdateEventDto = z.infer<typeof updateEventSchema>;
|
||||
export type EventResponseDto = z.infer<typeof eventResponseSchema>;
|
||||
20
backend/src/main.ts
Normal file
20
backend/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Enable CORS
|
||||
app.enableCors({
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3001',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
await app.listen(port);
|
||||
|
||||
console.log(`🚀 Application is running on: http://localhost:${port}`);
|
||||
console.log(`📊 Countdown endpoint: http://localhost:${port}/countdown`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
18
backend/src/pipes/zod-validation.pipe.ts
Normal file
18
backend/src/pipes/zod-validation.pipe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
|
||||
import type { ZodSchema } from 'zod';
|
||||
|
||||
@Injectable()
|
||||
export class ZodValidationPipe implements PipeTransform {
|
||||
constructor(private schema: ZodSchema) {}
|
||||
|
||||
transform(value: unknown) {
|
||||
try {
|
||||
const parsedValue = this.schema.parse(value);
|
||||
return parsedValue;
|
||||
} catch (error) {
|
||||
throw new BadRequestException('Validation failed', {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
197
backend/src/utils/datetime.utils.ts
Normal file
197
backend/src/utils/datetime.utils.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
addYears,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
differenceInMinutes,
|
||||
differenceInSeconds,
|
||||
isAfter,
|
||||
isBefore,
|
||||
setMonth,
|
||||
setDate,
|
||||
startOfDay,
|
||||
} from 'date-fns';
|
||||
|
||||
export type EventType = 'recurring' | 'anniversary' | 'duration';
|
||||
|
||||
/**
|
||||
* Определяет тип события по заполненным полям
|
||||
*/
|
||||
export function getEventType(event: {
|
||||
startYear?: number | null;
|
||||
endYear?: number | null;
|
||||
endMonth?: number | null;
|
||||
endDay?: number | null;
|
||||
}): EventType {
|
||||
const hasEndDate =
|
||||
event.endYear != null &&
|
||||
event.endMonth != null &&
|
||||
event.endDay != null;
|
||||
|
||||
if (hasEndDate) {
|
||||
return 'duration';
|
||||
}
|
||||
|
||||
if (event.startYear != null) {
|
||||
return 'anniversary';
|
||||
}
|
||||
|
||||
return 'recurring';
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает следующее наступление ежегодного события
|
||||
*/
|
||||
export function getNextOccurrence(
|
||||
now: Date,
|
||||
month: number,
|
||||
day: number,
|
||||
): Date {
|
||||
const currentYear = now.getFullYear();
|
||||
let target = new Date(currentYear, month - 1, day); // month is 1-indexed
|
||||
|
||||
// Если дата уже прошла в этом году, используем следующий год
|
||||
if (isBefore(target, now)) {
|
||||
target = addYears(target, 1);
|
||||
}
|
||||
|
||||
return startOfDay(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Расч итывает разницу во времени между двумя датами
|
||||
*/
|
||||
export function calculateTimeDiff(target: Date, now: Date): {
|
||||
display: string;
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
} {
|
||||
const diff = differenceInSeconds(target, now);
|
||||
|
||||
if (diff > 0) {
|
||||
const days = Math.floor(diff / 86400);
|
||||
const hours = Math.floor((diff % 86400) / 3600);
|
||||
const minutes = Math.floor((diff % 3600) / 60);
|
||||
|
||||
return {
|
||||
display: formatTimeRemaining(days, hours, minutes),
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
display: '✅ Свершилось!',
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует оставшееся время в читаемую строку
|
||||
*/
|
||||
export function formatTimeRemaining(
|
||||
days: number,
|
||||
hours: number,
|
||||
minutes: number,
|
||||
): string {
|
||||
return `${days}д ${hours}ч ${minutes}м`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает годовщину
|
||||
*/
|
||||
export function calculateAnniversary(
|
||||
startYear: number,
|
||||
month: number,
|
||||
day: number,
|
||||
now: Date,
|
||||
): {
|
||||
years: number;
|
||||
days: number;
|
||||
} {
|
||||
// Получаем следующую дату годовщины
|
||||
const nextOccurrence = getNextOccurrence(now, month, day);
|
||||
const years = nextOccurrence.getFullYear() - startYear;
|
||||
|
||||
// Дополнительные дни от прошлой годовщины
|
||||
const startDate = new Date(startYear, month - 1, day);
|
||||
const diffInDays = differenceInDays(now, startDate);
|
||||
const fullYears = Math.floor(diffInDays / 365.25);
|
||||
const remainingDays = diffInDays - Math.floor(fullYears * 365.25);
|
||||
|
||||
return {
|
||||
years,
|
||||
days: remainingDays,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает информацию о продолжительном событии
|
||||
*/
|
||||
export function calculateDuration(
|
||||
startMonth: number,
|
||||
startDay: number,
|
||||
startYear: number,
|
||||
endMonth: number,
|
||||
endDay: number,
|
||||
endYear: number,
|
||||
now: Date,
|
||||
): {
|
||||
isActive: boolean;
|
||||
daysUntilStart?: number;
|
||||
daysUntilEnd?: number;
|
||||
daysRemaining?: number;
|
||||
totalDays: number;
|
||||
} {
|
||||
const startDate = new Date(startYear, startMonth - 1, startDay);
|
||||
const endDate = new Date(endYear, endMonth - 1, endDay);
|
||||
const totalDays = differenceInDays(endDate, startDate);
|
||||
|
||||
const isActive = isAfter(now, startDate) && isBefore(now, endDate);
|
||||
|
||||
if (isBefore(now, startDate)) {
|
||||
// Событие еще не началось
|
||||
return {
|
||||
isActive: false,
|
||||
daysUntilStart: differenceInDays(startDate, now),
|
||||
totalDays,
|
||||
};
|
||||
}
|
||||
|
||||
if (isAfter(now, endDate)) {
|
||||
// Событие уже закончилось
|
||||
return {
|
||||
isActive: false,
|
||||
totalDays,
|
||||
};
|
||||
}
|
||||
|
||||
// Событие активно
|
||||
return {
|
||||
isActive: true,
|
||||
daysRemaining: differenceInDays(endDate, now),
|
||||
daysUntilEnd: differenceInDays(endDate, now),
|
||||
totalDays,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, идет ли событие прямо сейчас
|
||||
*/
|
||||
export function isEventActive(
|
||||
startMonth: number,
|
||||
startDay: number,
|
||||
startYear: number,
|
||||
endMonth: number,
|
||||
endDay: number,
|
||||
endYear: number,
|
||||
now: Date,
|
||||
): boolean {
|
||||
const startDate = new Date(startYear, startMonth - 1, startDay);
|
||||
const endDate = new Date(endYear, endMonth - 1, endDay);
|
||||
|
||||
return isAfter(now, startDate) && isBefore(now, endDate);
|
||||
}
|
||||
25
backend/test/app.e2e-spec.ts
Normal file
25
backend/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
9
backend/test/jest-e2e.json
Normal file
9
backend/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
4
backend/tsconfig.build.json
Normal file
4
backend/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
25
backend/tsconfig.json
Normal file
25
backend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolvePackageJsonExports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user