From 6dd552664693cbc79e40b4898c7beec3054f313b Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 28 Jun 2026 21:40:27 +0330 Subject: [PATCH] update database --- .dockerignore | 12 + .env.example | 8 +- captain-definition.json | 4 + package.json | 3 + src/app.module.ts | 13 +- src/database/data-source.ts | 13 + .../migrations/1719600000000-InitialSchema.ts | 223 ++++++++++++++++++ src/database/typeorm-options.ts | 19 ++ src/main.ts | 9 +- 9 files changed, 291 insertions(+), 13 deletions(-) create mode 100644 .dockerignore create mode 100644 captain-definition.json create mode 100644 src/database/data-source.ts create mode 100644 src/database/migrations/1719600000000-InitialSchema.ts create mode 100644 src/database/typeorm-options.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1af0a29 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.env +.env.* +!.env.example +node_modules +dist +coverage +.git +.github +*.md +test +.vscode +.idea diff --git a/.env.example b/.env.example index 1e0296a..ba23948 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,9 @@ DB_USERNAME= DB_PASSWORD= DB_NAME= +# CapRover production example: +# DB_HOST=srv-captain--growdb + # SMS SMS_API_KEY= SMS_SENDER= @@ -24,5 +27,8 @@ JWT_REFRESH_SECRET= JWT_REFRESH_EXPIRES_IN=7d # App -PORT=3000 +PORT=4000 NODE_ENV=development + +# CORS (comma-separated origins, e.g. https://app.example.com) +# CORS_ORIGIN= diff --git a/captain-definition.json b/captain-definition.json new file mode 100644 index 0000000..0e14f82 --- /dev/null +++ b/captain-definition.json @@ -0,0 +1,4 @@ +{ + "schemaVersion": 2, + "dockerfilePath": "./Dockerfile" +} diff --git a/package.json b/package.json index e7fc976..771ccfd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "start:dev": "NODE_ENV=development nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", + "migration:run": "typeorm migration:run -d dist/database/data-source.js", + "migration:revert": "typeorm migration:revert -d dist/database/data-source.js", + "migration:generate": "typeorm migration:generate -d src/database/data-source.ts", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", diff --git a/src/app.module.ts b/src/app.module.ts index e9a58d0..4e2d271 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthModule } from './auth/auth.module'; import { CustomersModule } from './customers/customers.module'; import configuration from './config/configuration'; +import { getTypeOrmOptions } from './database/typeorm-options'; import { SalonsModule } from './salons/salons.module'; import { UsersModule } from './users/users.module'; @@ -13,17 +14,7 @@ import { UsersModule } from './users/users.module'; isGlobal: true, load: [configuration], }), - TypeOrmModule.forRoot({ - type: 'postgres', - host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT as string), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - autoLoadEntities: true, - entities: [__dirname + '/**/*.entity.{js,ts}'], - synchronize: process.env.NODE_ENV !== 'production', - }), + TypeOrmModule.forRoot(getTypeOrmOptions()), AuthModule, UsersModule, SalonsModule, diff --git a/src/database/data-source.ts b/src/database/data-source.ts new file mode 100644 index 0000000..d5a94f8 --- /dev/null +++ b/src/database/data-source.ts @@ -0,0 +1,13 @@ +import 'dotenv/config'; +import { DataSource } from 'typeorm'; + +export default new DataSource({ + type: 'postgres', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT ?? '5432', 10), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + entities: ['src/**/*.entity.ts'], + migrations: ['src/database/migrations/*.ts'], +}); diff --git a/src/database/migrations/1719600000000-InitialSchema.ts b/src/database/migrations/1719600000000-InitialSchema.ts new file mode 100644 index 0000000..331796b --- /dev/null +++ b/src/database/migrations/1719600000000-InitialSchema.ts @@ -0,0 +1,223 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class InitialSchema1719600000000 implements MigrationInterface { + name = 'InitialSchema1719600000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."users_role_enum" AS ENUM('admin', 'salon-owner', 'customer')`, + ); + + await queryRunner.createTable( + new Table({ + name: 'users', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'firstName', type: 'varchar', length: '100' }, + { name: 'lastName', type: 'varchar', length: '100' }, + { name: 'mobile', type: 'varchar', length: '11', isUnique: true }, + { + name: 'role', + type: 'users_role_enum', + default: `'customer'`, + }, + { name: 'createdAt', type: 'timestamp', default: 'now()' }, + { name: 'updatedAt', type: 'timestamp', default: 'now()' }, + ], + }), + true, + ); + + await queryRunner.createTable( + new Table({ + name: 'salons', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'name', type: 'varchar', length: '200' }, + { name: 'description', type: 'text', isNullable: true }, + { name: 'address', type: 'varchar', length: '500' }, + { name: 'phone', type: 'varchar', length: '11' }, + { name: 'ownerId', type: 'uuid' }, + { name: 'createdAt', type: 'timestamp', default: 'now()' }, + { name: 'updatedAt', type: 'timestamp', default: 'now()' }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'salons', + new TableForeignKey({ + columnNames: ['ownerId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createTable( + new Table({ + name: 'customers', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'userId', type: 'uuid', isUnique: true }, + { name: 'dateOfBirth', type: 'date' }, + { name: 'address', type: 'varchar', length: '500' }, + { name: 'createdAt', type: 'timestamp', default: 'now()' }, + { name: 'updatedAt', type: 'timestamp', default: 'now()' }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'customers', + new TableForeignKey({ + columnNames: ['userId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createTable( + new Table({ + name: 'otps', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'mobile', type: 'varchar', length: '11' }, + { name: 'code', type: 'varchar' }, + { name: 'expiresAt', type: 'timestamp' }, + { name: 'attempts', type: 'int', default: 0 }, + { name: 'verified', type: 'boolean', default: false }, + { name: 'createdAt', type: 'timestamp', default: 'now()' }, + ], + indices: [{ columnNames: ['mobile'] }], + }), + true, + ); + + await queryRunner.createTable( + new Table({ + name: 'refresh_tokens', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'userId', type: 'uuid' }, + { name: 'tokenHash', type: 'varchar' }, + { name: 'expiresAt', type: 'timestamp' }, + { name: 'revokedAt', type: 'timestamp', isNullable: true }, + { name: 'createdAt', type: 'timestamp', default: 'now()' }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'refresh_tokens', + new TableForeignKey({ + columnNames: ['userId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createTable( + new Table({ + name: 'permissions', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'code', type: 'varchar', isUnique: true }, + { name: 'description', type: 'varchar' }, + ], + }), + true, + ); + + await queryRunner.createTable( + new Table({ + name: 'user_permissions', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'userId', type: 'uuid' }, + { name: 'permissionId', type: 'uuid' }, + ], + uniques: [{ columnNames: ['userId', 'permissionId'] }], + }), + true, + ); + + await queryRunner.createForeignKey( + 'user_permissions', + new TableForeignKey({ + columnNames: ['userId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createForeignKey( + 'user_permissions', + new TableForeignKey({ + columnNames: ['permissionId'], + referencedTableName: 'permissions', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('user_permissions', true); + await queryRunner.dropTable('permissions', true); + await queryRunner.dropTable('refresh_tokens', true); + await queryRunner.dropTable('otps', true); + await queryRunner.dropTable('customers', true); + await queryRunner.dropTable('salons', true); + await queryRunner.dropTable('users', true); + await queryRunner.query(`DROP TYPE "public"."users_role_enum"`); + } +} diff --git a/src/database/typeorm-options.ts b/src/database/typeorm-options.ts new file mode 100644 index 0000000..96b3048 --- /dev/null +++ b/src/database/typeorm-options.ts @@ -0,0 +1,19 @@ +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; + +export function getTypeOrmOptions(): TypeOrmModuleOptions { + const isProduction = process.env.NODE_ENV === 'production'; + + return { + type: 'postgres', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT ?? '5432', 10), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + autoLoadEntities: true, + entities: [__dirname + '/../**/*.entity.{js,ts}'], + migrations: [__dirname + '/migrations/*.{js,ts}'], + synchronize: !isProduction, + migrationsRun: isProduction, + }; +} diff --git a/src/main.ts b/src/main.ts index 34aba4b..bc7b628 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,14 @@ async function bootstrap() { app.setGlobalPrefix('api'); // Cors - app.enableCors(); + const corsOrigin = process.env.CORS_ORIGIN; + if (corsOrigin) { + app.enableCors({ + origin: corsOrigin.split(',').map((origin) => origin.trim()), + }); + } else { + app.enableCors(); + } await app.listen(process.env.PORT ?? 3000); }