diff --git a/backend/.eslintrc.yml b/backend/.eslintrc.yml index 4f96e782..75982e1c 100644 --- a/backend/.eslintrc.yml +++ b/backend/.eslintrc.yml @@ -36,6 +36,9 @@ settings: - .ts rules: + class-methods-use-this: + - 2 + - exceptMethods: ["up", "down"] import/no-cycle: [2, { maxDepth: 1 }] import/prefer-default-export: 0 import/extensions: 0 diff --git a/backend/Makefile b/backend/Makefile index 2fb2cc77..039ff938 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -4,10 +4,21 @@ install: npm install db-migrate: - npm run typeorm -- migration:run -d src/data-source + npm run typeorm -- migration:run -d src/data-source.ts db-generate: - npm run typeorm -- migration:generate src/migrations/migrations -d src/data-source + npm run typeorm -- migration:generate src/migrations -d src/data-source + +db-revert: + npm run typeorm -- migration:revert -d src/data-source.ts + +db-refresh: db-revert db-migrate + +db-create-migration: + npm run typeorm migration:create src/migrations/$(NAME) + +db-drop: + npm run typeorm -- schema:drop -d src/data-source.ts start: npm run start diff --git a/backend/__tests__/auth.e2e-spec.ts b/backend/__tests__/auth.e2e-spec.ts index 45355e63..0b47c98b 100644 --- a/backend/__tests__/auth.e2e-spec.ts +++ b/backend/__tests__/auth.e2e-spec.ts @@ -7,7 +7,7 @@ import { Repository } from 'typeorm'; import { ValidationPipe } from '@nestjs/common'; import * as cookieParser from 'cookie-parser'; import { JwtService } from '@nestjs/jwt'; -import { Users } from '../src/entities/user.entity'; +import { User } from '../src/entities/user.entity'; import { AuthModule } from '../src/auth/auth.module'; import getDataSourceConfig from '../src/config/data-source.config'; import { AppModule } from '../src/app.module'; @@ -15,11 +15,11 @@ import { UsersModule } from '../src/users/users.module'; describe('AuthController (e2e)', () => { let app: NestExpressApplication; - let usersRepo: Repository; + let usersRepo: Repository; let testData: Record; let users: Array>; let moduleFixture: TestingModule; - let usersData: Users[]; + let usersData: User[]; let jwtService: JwtService; // FIXME: все тесты проходят если beforeEach @@ -32,7 +32,7 @@ describe('AuthController (e2e)', () => { UsersModule, AuthModule, TypeOrmModule.forRoot(getDataSourceConfig()), - TypeOrmModule.forFeature([Users]), + TypeOrmModule.forFeature([User]), ], }).compile(); diff --git a/backend/__tests__/snippets.e2e-spec.ts b/backend/__tests__/snippets.e2e-spec.ts index fd4d3476..dd24ecc5 100644 --- a/backend/__tests__/snippets.e2e-spec.ts +++ b/backend/__tests__/snippets.e2e-spec.ts @@ -12,20 +12,20 @@ import { AuthModule } from '../src/auth/auth.module'; import { AppService } from '../src/app.service'; import { UsersService } from '../src/users/users.service'; import { AppModule } from '../src/app.module'; -import { Users } from '../src/entities/user.entity'; -import { Snippets } from '../src/entities/snippet.entity'; +import { User } from '../src/entities/user.entity'; +import { Snippet } from '../src/entities/snippet.entity'; import getDataSourceConfig from '../src/config/data-source.config'; describe('SnippetsController (e2e)', () => { let app: NestExpressApplication; - let usersRepo: Repository; - let snippetsRepo: Repository; + let usersRepo: Repository; + let snippetsRepo: Repository; let testData: Record; let users: Array>; let snippets: Array>; let moduleFixture: TestingModule; - let usersData: Users[]; - let snippetsData: Snippets[]; + let usersData: User[]; + let snippetsData: Snippet[]; let jwtService: JwtService; let token: string; @@ -36,7 +36,7 @@ describe('SnippetsController (e2e)', () => { UsersModule, AuthModule, TypeOrmModule.forRoot(getDataSourceConfig()), - TypeOrmModule.forFeature([Users, Snippets]), + TypeOrmModule.forFeature([User, Snippet]), ], providers: [AppService, UsersService], }).compile(); diff --git a/backend/__tests__/users.e2e-spec.ts b/backend/__tests__/users.e2e-spec.ts index 2065dc8a..5d3ffb6b 100644 --- a/backend/__tests__/users.e2e-spec.ts +++ b/backend/__tests__/users.e2e-spec.ts @@ -14,20 +14,20 @@ import { AppService } from '../src/app.service'; import { UsersService } from '../src/users/users.service'; import { CheckEmail } from '../src/users/validation/check-email'; import { AppModule } from '../src/app.module'; -import { Users } from '../src/entities/user.entity'; -import { Snippets } from '../src/entities/snippet.entity'; +import { User } from '../src/entities/user.entity'; +import { Snippet } from '../src/entities/snippet.entity'; import getDataSourceConfig from '../src/config/data-source.config'; describe('UsersController (e2e)', () => { let app: NestExpressApplication; - let usersRepo: Repository; - let snippetsRepo: Repository; + let usersRepo: Repository; + let snippetsRepo: Repository; let testData: Record; let users: Array>; let snippets: Array>; let moduleFixture: TestingModule; - let usersData: Users[]; - let snippetsData: Snippets[]; + let usersData: User[]; + let snippetsData: Snippet[]; let jwtService: JwtService; let token: string; @@ -38,7 +38,7 @@ describe('UsersController (e2e)', () => { UsersModule, AuthModule, TypeOrmModule.forRoot(getDataSourceConfig()), - TypeOrmModule.forFeature([Users, Snippets]), + TypeOrmModule.forFeature([User, Snippet]), ], providers: [AppService, UsersService, CheckEmail], }).compile(); diff --git a/backend/src/config/data-source.config.ts b/backend/src/config/data-source.config.ts index 937d92a8..d34f84b3 100644 --- a/backend/src/config/data-source.config.ts +++ b/backend/src/config/data-source.config.ts @@ -1,17 +1,14 @@ import { DataSourceOptions } from 'typeorm'; -import { Users } from '../entities/user.entity'; -import { Snippets } from '../entities/snippet.entity'; -import { migration1663236009774 } from '../migrations/1663236009774-migration'; -import { migration1670352324202 } from '../migrations/1670352324202-migration'; -import { AdduserRecoverHash1677580680097 } from '../migrations/1677580680097-AdduserRecoverHash'; -import { FillNullSlugsSnippets1682678760453 } from '../migrations/1682678760453-fill-null-slugs-snippets'; -import { RenameLoginToUsername1691073864288 } from '../migrations/1691073864288-RenameLoginToUsername'; -import { Migrations1699462100238 } from '../migrations/1699462100238-migrations'; export default (): DataSourceOptions => { + const defaultConfig = { + entities: [`${__dirname}/../entities/**/*{.ts,.js}`], + migrations: [`${__dirname}/../migrations/**/*{.ts,.js}`], + }; switch (process.env.NODE_ENV) { case 'production': return { + ...defaultConfig, type: 'postgres', username: process.env.DATABASE_USERNAME, password: process.env.DATABASE_PASSWORD, @@ -19,43 +16,20 @@ export default (): DataSourceOptions => { url: process.env.DATABASE_URL, synchronize: false, ssl: { rejectUnauthorized: false }, - entities: [Users, Snippets], - migrations: [ - migration1663236009774, - migration1670352324202, - AdduserRecoverHash1677580680097, - FillNullSlugsSnippets1682678760453, - RenameLoginToUsername1691073864288, - Migrations1699462100238, - ], }; case 'test': return { + ...defaultConfig, type: 'sqlite', database: ':memory:', synchronize: true, - entities: [Users, Snippets], - migrations: [ - migration1663236009774, - migration1670352324202, - AdduserRecoverHash1677580680097, - RenameLoginToUsername1691073864288, - Migrations1699462100238, - ], }; default: return { + ...defaultConfig, type: 'sqlite', database: 'runit.sqlite', synchronize: false, - entities: [Users, Snippets], - migrations: [ - migration1663236009774, - migration1670352324202, - AdduserRecoverHash1677580680097, - RenameLoginToUsername1691073864288, - Migrations1699462100238, - ], }; } }; diff --git a/backend/src/entities/snippet.entity.ts b/backend/src/entities/snippet.entity.ts index 67845505..3e7d96b3 100644 --- a/backend/src/entities/snippet.entity.ts +++ b/backend/src/entities/snippet.entity.ts @@ -7,10 +7,10 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import type { Users } from './user.entity'; +import type { User } from './user.entity'; -@Entity() -export class Snippets { +@Entity('snippets') +export class Snippet { @PrimaryGeneratedColumn() id: number; @@ -26,9 +26,9 @@ export class Snippets { @Column('text') language: string; - @ManyToOne('Users', 'snippets', { onDelete: 'CASCADE' }) + @ManyToOne('User', 'snippets') @JoinColumn() - user: Users; + user: User; @CreateDateColumn() created_at: string; diff --git a/backend/src/entities/user.entity.ts b/backend/src/entities/user.entity.ts index 2000f3a9..8ad030f4 100644 --- a/backend/src/entities/user.entity.ts +++ b/backend/src/entities/user.entity.ts @@ -11,10 +11,10 @@ import { UpdateDateColumn, } from 'typeorm'; import { encrypt } from '../users/secure/encrypt'; -import type { Snippets } from './snippet.entity'; +import type { Snippet } from './snippet.entity'; -@Entity() -export class Users { +@Entity('users') +export class User { @PrimaryGeneratedColumn() id: number; @@ -28,8 +28,8 @@ export class Users { @Column('text') password: string; - @OneToMany('Snippets', 'user') - snippets: Snippets[]; + @OneToMany('Snippet', 'user') + snippets: Snippet[]; @Column({ nullable: true }) recover_hash: string; diff --git a/backend/src/migrations/1663236009774-migration.ts b/backend/src/migrations/1663236009774-migration.ts deleted file mode 100644 index fba897c9..00000000 --- a/backend/src/migrations/1663236009774-migration.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { MigrationInterface, QueryRunner, Table } from 'typeorm'; - -export class migration1663236009774 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - name: 'users', - columns: [ - { - name: 'id', - type: 'integer', - isGenerated: true, - generatedIdentity: 'ALWAYS', - generationStrategy: 'increment', - isNullable: false, - isPrimary: true, - }, - { - name: 'login', - type: 'varchar(20)', - isNullable: false, - isUnique: true, - }, - { - name: 'email', - type: 'varchar(60)', - isNullable: false, - isUnique: true, - }, - { - name: 'password', - type: 'varchar(60)', - isNullable: false, - }, - { - name: 'created_at', - type: 'timestamp', - isNullable: false, - default: 'CURRENT_TIMESTAMP', - }, - { - name: 'updated_at', - type: 'timestamp', - isNullable: false, - default: 'CURRENT_TIMESTAMP', - }, - ], - }), - ); - - await queryRunner.createTable( - new Table({ - name: 'snippets', - columns: [ - { - name: 'id', - type: 'integer', - isGenerated: true, - generatedIdentity: 'ALWAYS', - generationStrategy: 'increment', - isNullable: false, - isPrimary: true, - }, - { - name: 'name', - type: 'varchar(30)', - isNullable: false, - default: "'Untitled'", - }, - { - name: 'code', - type: 'text', - isNullable: false, - }, - { - name: 'created_at', - type: 'timestamp', - isNullable: false, - default: 'CURRENT_TIMESTAMP', - }, - { - name: 'updated_at', - type: 'timestamp', - isNullable: false, - default: 'CURRENT_TIMESTAMP', - }, - { - name: 'userId', - type: 'int', - }, - ], - foreignKeys: [ - { - name: 'userId', - columnNames: ['userId'], - referencedTableName: 'users', - referencedColumnNames: ['id'], - onUpdate: 'cascade', - onDelete: 'cascade', - }, - ], - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('users'); - await queryRunner.dropTable('snippets'); - } -} diff --git a/backend/src/migrations/1670352324202-migration.ts b/backend/src/migrations/1670352324202-migration.ts deleted file mode 100644 index 8df2764f..00000000 --- a/backend/src/migrations/1670352324202-migration.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class migration1670352324202 implements MigrationInterface { - name = 'migration1670352324202'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "snippets" ADD COLUMN "slug" varchar(30)`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "snippets" DELETE COLUMN "slug"`); - } -} diff --git a/backend/src/migrations/1673189643179-migrations.ts b/backend/src/migrations/1673189643179-migrations.ts deleted file mode 100644 index 2d8fa762..00000000 --- a/backend/src/migrations/1673189643179-migrations.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint class-methods-use-this: ["error", { "exceptMethods": ["up", "down"] }] */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class migrations1673189643179 implements MigrationInterface { - name = 'migrations1673189643179'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "login" varchar NOT NULL, "email" varchar NOT NULL, "password" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_2d443082eccd5198f95f2a36e2c" UNIQUE ("login"))`, - ); - await queryRunner.query( - `CREATE UNIQUE INDEX "IDX_97672ac88f789774dd47f7c8be" ON "users" ("email") `, - ); - await queryRunner.query( - `CREATE TABLE "snippets" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL DEFAULT ('Untitled'), "code" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer)`, - ); - await queryRunner.query( - `CREATE TABLE "temporary_snippets" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL DEFAULT ('Untitled'), "code" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, CONSTRAINT "FK_8fdfc80b4a5bf0ac48946a2ca1f" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`, - ); - await queryRunner.query( - `INSERT INTO "temporary_snippets"("id", "name", "code", "created_at", "updated_at", "userId") SELECT "id", "name", "code", "created_at", "updated_at", "userId" FROM "snippets"`, - ); - await queryRunner.query(`DROP TABLE "snippets"`); - await queryRunner.query( - `ALTER TABLE "temporary_snippets" RENAME TO "snippets"`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "snippets" RENAME TO "temporary_snippets"`, - ); - await queryRunner.query( - `CREATE TABLE "snippets" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL DEFAULT ('Untitled'), "code" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer)`, - ); - await queryRunner.query( - `INSERT INTO "snippets"("id", "name", "code", "created_at", "updated_at", "userId") SELECT "id", "name", "code", "created_at", "updated_at", "userId" FROM "temporary_snippets"`, - ); - await queryRunner.query(`DROP TABLE "temporary_snippets"`); - await queryRunner.query(`DROP TABLE "snippets"`); - await queryRunner.query(`DROP INDEX "IDX_97672ac88f789774dd47f7c8be"`); - await queryRunner.query(`DROP TABLE "users"`); - } -} diff --git a/backend/src/migrations/1673189675326-migrations.ts b/backend/src/migrations/1673189675326-migrations.ts deleted file mode 100644 index bb5488e9..00000000 --- a/backend/src/migrations/1673189675326-migrations.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint class-methods-use-this: ["error", { "exceptMethods": ["up", "down"] }] */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class migrations1673189675326 implements MigrationInterface { - name = 'migrations1673189675326'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "login" varchar NOT NULL, "email" varchar NOT NULL, "password" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_2d443082eccd5198f95f2a36e2c" UNIQUE ("login"))`, - ); - await queryRunner.query( - `CREATE UNIQUE INDEX "IDX_97672ac88f789774dd47f7c8be" ON "users" ("email") `, - ); - await queryRunner.query( - `CREATE TABLE "snippets" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL DEFAULT ('Untitled'), "code" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer)`, - ); - await queryRunner.query( - `CREATE TABLE "temporary_snippets" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL DEFAULT ('Untitled'), "code" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, CONSTRAINT "FK_8fdfc80b4a5bf0ac48946a2ca1f" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`, - ); - await queryRunner.query( - `INSERT INTO "temporary_snippets"("id", "name", "code", "created_at", "updated_at", "userId") SELECT "id", "name", "code", "created_at", "updated_at", "userId" FROM "snippets"`, - ); - await queryRunner.query(`DROP TABLE "snippets"`); - await queryRunner.query( - `ALTER TABLE "temporary_snippets" RENAME TO "snippets"`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "snippets" RENAME TO "temporary_snippets"`, - ); - await queryRunner.query( - `CREATE TABLE "snippets" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL DEFAULT ('Untitled'), "code" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer)`, - ); - await queryRunner.query( - `INSERT INTO "snippets"("id", "name", "code", "created_at", "updated_at", "userId") SELECT "id", "name", "code", "created_at", "updated_at", "userId" FROM "temporary_snippets"`, - ); - await queryRunner.query(`DROP TABLE "temporary_snippets"`); - await queryRunner.query(`DROP TABLE "snippets"`); - await queryRunner.query(`DROP INDEX "IDX_97672ac88f789774dd47f7c8be"`); - await queryRunner.query(`DROP TABLE "users"`); - } -} diff --git a/backend/src/migrations/1677580680097-AdduserRecoverHash.ts b/backend/src/migrations/1677580680097-AdduserRecoverHash.ts deleted file mode 100644 index 90808689..00000000 --- a/backend/src/migrations/1677580680097-AdduserRecoverHash.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint class-methods-use-this: ["error", { "exceptMethods": ["up", "down"] }] */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AdduserRecoverHash1677580680097 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "users" ADD COLUMN "recover_hash" varchar(50) DEFAULT NULL`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "users" DROP COLUMN recover_hash`); - } -} diff --git a/backend/src/migrations/1682678760453-fill-null-slugs-snippets.ts b/backend/src/migrations/1682678760453-fill-null-slugs-snippets.ts deleted file mode 100644 index 18f3bddb..00000000 --- a/backend/src/migrations/1682678760453-fill-null-slugs-snippets.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable no-console */ -/* eslint class-methods-use-this: ["error", { "exceptMethods": ["up", "down"] }] */ -import { MigrationInterface, QueryRunner } from 'typeorm'; -import { generateUniqSlug } from '../snippets/utils/generate-uniq-slug'; -import { Snippets } from '../entities/snippet.entity'; - -export class FillNullSlugsSnippets1682678760453 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const snippets: Snippets[] = await queryRunner.query( - `SELECT id, slug FROM snippets;`, - ); - const nullSnippets = snippets.filter((snippet) => !snippet.slug); - - nullSnippets.forEach(async (nullSnippet) => { - const slug = generateUniqSlug(snippets); - - await queryRunner.query( - `UPDATE snippets SET slug='${slug}' WHERE id=${nullSnippet.id};`, - ); - }); - } - - public async down(): Promise { - console.log( - `Revert migrations for filling null slugs in snippets is not specified!`, - ); - } -} diff --git a/backend/src/migrations/1691073864288-RenameLoginToUsername.ts b/backend/src/migrations/1691073864288-RenameLoginToUsername.ts deleted file mode 100644 index fd37962e..00000000 --- a/backend/src/migrations/1691073864288-RenameLoginToUsername.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint class-methods-use-this: ["error", { "exceptMethods": ["up", "down"] }] */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class RenameLoginToUsername1691073864288 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "users" RENAME COLUMN "login" TO "username"`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "users" RENAME COLUMN "username" TO "login"`, - ); - } -} diff --git a/backend/src/migrations/1699462100238-migrations.ts b/backend/src/migrations/1699462100238-migrations.ts index 9ae9d846..dba22d7d 100644 --- a/backend/src/migrations/1699462100238-migrations.ts +++ b/backend/src/migrations/1699462100238-migrations.ts @@ -1,14 +1,122 @@ -/* eslint class-methods-use-this: ["error", { "exceptMethods": ["up", "down"] }] */ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; export class Migrations1699462100238 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "snippets" ADD COLUMN "language" varchar(50) DEFAULT NULL`, + await queryRunner.createTable( + new Table({ + name: 'users', + columns: [ + { + name: 'id', + type: 'integer', + isGenerated: true, + generatedIdentity: 'ALWAYS', + generationStrategy: 'increment', + isNullable: false, + isPrimary: true, + }, + { + name: 'username', + type: 'varchar(20)', + isNullable: false, + isUnique: true, + }, + { + name: 'email', + type: 'varchar(60)', + isNullable: false, + isUnique: true, + }, + { + name: 'password', + type: 'varchar(60)', + isNullable: false, + }, + { + name: 'recover_hash', + type: 'varchar(50)', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'updated_at', + type: 'timestamp', + isNullable: false, + default: 'CURRENT_TIMESTAMP', + }, + ], + }), + ); + + await queryRunner.createTable( + new Table({ + name: 'snippets', + columns: [ + { + name: 'id', + type: 'integer', + isGenerated: true, + generatedIdentity: 'ALWAYS', + generationStrategy: 'increment', + isNullable: false, + isPrimary: true, + }, + { + name: 'name', + type: 'varchar(30)', + isNullable: false, + }, + { + name: 'slug', + type: 'varchar(30)', + isNullable: true, + }, + { + name: 'code', + type: 'text', + isNullable: false, + }, + { + name: 'language', + type: 'varchar(50)', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'updated_at', + type: 'timestamp', + isNullable: false, + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'userId', + type: 'int', + }, + ], + foreignKeys: [ + { + name: 'userId', + columnNames: ['userId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + }, + ], + }), ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "snippets" DROP COLUMN language`); + await queryRunner.query(`DROP TABLE "snippets"`); + await queryRunner.query(`DROP TABLE "users"`); } } diff --git a/backend/src/migrations/1717697901307-fix-old-snippets-language.ts b/backend/src/migrations/1717697901307-fix-old-snippets-language.ts new file mode 100644 index 00000000..405b7022 --- /dev/null +++ b/backend/src/migrations/1717697901307-fix-old-snippets-language.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { Snippet } from '../entities/snippet.entity'; + +export class FixOldSnippetsLanguage1717697901307 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.manager + .getRepository(Snippet) + .createQueryBuilder() + .update() + .set({ language: 'javascript' }) + .where('language IS NULL') + .execute(); + } + + public async down(): Promise { + // nothing + } +} diff --git a/backend/src/snippets/interfaces/snippets.interface.ts b/backend/src/snippets/interfaces/snippet.interface.ts similarity index 64% rename from backend/src/snippets/interfaces/snippets.interface.ts rename to backend/src/snippets/interfaces/snippet.interface.ts index 341615d9..de37241f 100644 --- a/backend/src/snippets/interfaces/snippets.interface.ts +++ b/backend/src/snippets/interfaces/snippet.interface.ts @@ -1,4 +1,4 @@ -export interface Snippet { +export interface ISnippet { name: string; code: any; language: string; diff --git a/backend/src/snippets/snippets.controller.ts b/backend/src/snippets/snippets.controller.ts index 54646e93..8d6207c7 100644 --- a/backend/src/snippets/snippets.controller.ts +++ b/backend/src/snippets/snippets.controller.ts @@ -24,13 +24,13 @@ import { import { User as UserDecorator } from '../users/users.decorator'; import { CreateSnippetDto } from './dto/create-snippet.dto'; import { UpdateSnippetDto } from './dto/update-snippet.dto'; -import { Snippet } from './interfaces/snippets.interface'; +import { ISnippet } from './interfaces/snippet.interface'; import { SnippetsService } from './snippets.service'; import { HttpExceptionFilter } from './exceptions/http-exception.filter'; import { ParseIntPipe } from './pipes/parse-int.pipe'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { ValidationPipe } from './validation/validation.pipe'; -import { User } from '../users/interfaces/users.interface'; +import { IUser } from '../users/interfaces/users.interface'; @ApiTags('snippets') @Controller('snippets') @@ -40,7 +40,7 @@ export class SnippetsController { @Get() @ApiOkResponse({ description: 'Successfully returned all snippets' }) - async findAll(): Promise { + async findAll(): Promise { return this.snippetsService.findAll(); } @@ -57,13 +57,15 @@ export class SnippetsController { async findOneByUsernameSlug( @Param('username') username: string, @Param('slug') slug: string, - ): Promise { + ): Promise { return this.snippetsService.findByUsernameSlug(username, slug); } @Get(':id') @ApiOkResponse({ description: 'Successfully returned snippet by id' }) - async findOne(@Param('id', new ParseIntPipe()) id: number): Promise { + async findOne( + @Param('id', new ParseIntPipe()) id: number, + ): Promise { return this.snippetsService.findOne(id); } @@ -74,7 +76,7 @@ export class SnippetsController { @ApiBadRequestResponse({ description: 'Validation failed!' }) @ApiCreatedResponse({ description: 'Snippet successfully created!' }) async create( - @UserDecorator('user') user: User, + @UserDecorator('user') user: IUser, @Body(new ValidationPipe()) createSnippetDto: CreateSnippetDto, ) { diff --git a/backend/src/snippets/snippets.module.ts b/backend/src/snippets/snippets.module.ts index ca5863e1..45169dad 100644 --- a/backend/src/snippets/snippets.module.ts +++ b/backend/src/snippets/snippets.module.ts @@ -2,15 +2,15 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtService } from '@nestjs/jwt'; import { AuthService } from '../auth/auth.service'; -import { Users } from '../entities/user.entity'; -import { Snippets } from '../entities/snippet.entity'; +import { User } from '../entities/user.entity'; +import { Snippet } from '../entities/snippet.entity'; import { SnippetsController } from './snippets.controller'; import { SnippetsService } from './snippets.service'; import { SnippetSubscriber } from './snippets.subscriber'; import { UsersService } from '../users/users.service'; @Module({ - imports: [TypeOrmModule.forFeature([Snippets, Users])], + imports: [TypeOrmModule.forFeature([Snippet, User])], controllers: [SnippetsController], providers: [ SnippetsService, diff --git a/backend/src/snippets/snippets.service.ts b/backend/src/snippets/snippets.service.ts index fd2b41ee..c7dbb9a5 100644 --- a/backend/src/snippets/snippets.service.ts +++ b/backend/src/snippets/snippets.service.ts @@ -4,11 +4,11 @@ import { Injectable } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { EntityManager, Repository } from 'typeorm'; import { faker } from '@faker-js/faker/locale/en'; -import { Users } from '../entities/user.entity'; +import { User } from '../entities/user.entity'; import { CreateSnippetDto } from './dto/create-snippet.dto'; import { UpdateSnippetDto } from './dto/update-snippet.dto'; -import { Snippets } from '../entities/snippet.entity'; -import { User } from '../users/interfaces/users.interface'; +import { Snippet } from '../entities/snippet.entity'; +import { IUser } from '../users/interfaces/users.interface'; import { generateUniqSlug } from './utils/generate-uniq-slug'; @Injectable() @@ -16,13 +16,13 @@ export class SnippetsService { constructor( @InjectEntityManager() private snippetManager: EntityManager, - @InjectRepository(Snippets) - private snippetsRepository: Repository, - @InjectRepository(Users) - private usersRepository: Repository, + @InjectRepository(Snippet) + private snippetsRepository: Repository, + @InjectRepository(User) + private usersRepository: Repository, ) {} - async findOne(id: number): Promise { + async findOne(id: number): Promise { return this.snippetsRepository.findOneBy({ id }); } @@ -41,7 +41,7 @@ export class SnippetsService { async getSlug(id: number): Promise { const snippets = await this.snippetManager - .createQueryBuilder(Snippets, 'snippet') + .createQueryBuilder(Snippet, 'snippet') .where('snippet.userId= :id', { id }) .getMany(); return generateUniqSlug(snippets); @@ -49,9 +49,9 @@ export class SnippetsService { async create( createSnippetDto: CreateSnippetDto, - { id }: User, - ): Promise { - const snippet = new Snippets(); + { id }: IUser, + ): Promise { + const snippet = new Snippet(); const { name, code, language } = createSnippetDto; const user = await this.usersRepository.findOneBy({ id }); snippet.slug = await this.getSlug(id); @@ -65,7 +65,7 @@ export class SnippetsService { async update( id: number, updateSnippetDto: UpdateSnippetDto, - ): Promise { + ): Promise { await this.snippetsRepository.update(id, updateSnippetDto); return this.snippetsRepository.findOneBy({ id }); } @@ -74,7 +74,7 @@ export class SnippetsService { await this.snippetsRepository.delete(id); } - findAll(): Promise { + findAll(): Promise { return this.snippetsRepository.find(); } diff --git a/backend/src/snippets/snippets.subscriber.ts b/backend/src/snippets/snippets.subscriber.ts index e7578354..6004f471 100644 --- a/backend/src/snippets/snippets.subscriber.ts +++ b/backend/src/snippets/snippets.subscriber.ts @@ -6,19 +6,19 @@ import { EventSubscriber, InsertEvent, } from 'typeorm'; -import { Snippets } from '../entities/snippet.entity'; +import { Snippet } from '../entities/snippet.entity'; @EventSubscriber() -export class SnippetSubscriber implements EntitySubscriberInterface { +export class SnippetSubscriber implements EntitySubscriberInterface { constructor(dataSource: DataSource) { dataSource.subscribers.push(this); } listenTo() { - return Snippets; + return Snippet; } - beforeInsert(event: InsertEvent): void | Promise { + beforeInsert(event: InsertEvent): void | Promise { console.log('BEFORE SNIPPET INSERTER: ', event.entity); } } diff --git a/backend/src/snippets/utils/generate-uniq-slug.ts b/backend/src/snippets/utils/generate-uniq-slug.ts index 51dd1177..14296437 100644 --- a/backend/src/snippets/utils/generate-uniq-slug.ts +++ b/backend/src/snippets/utils/generate-uniq-slug.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; -import { Snippets } from '../../entities/snippet.entity'; +import { Snippet } from '../../entities/snippet.entity'; -export const generateUniqSlug = (snippets: Snippets[]): string => { +export const generateUniqSlug = (snippets: Snippet[]): string => { const slug = faker.random.alpha({ count: 7, casing: 'mixed' }); return !snippets.find((snippet) => snippet.slug === slug) ? slug diff --git a/backend/src/users/interfaces/users.interface.ts b/backend/src/users/interfaces/users.interface.ts index 635b06b9..e0da0c2a 100644 --- a/backend/src/users/interfaces/users.interface.ts +++ b/backend/src/users/interfaces/users.interface.ts @@ -1,4 +1,4 @@ -export interface User { +export interface IUser { id: number; username: string; email: string; diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts index 3e69d173..9434eded 100644 --- a/backend/src/users/users.controller.ts +++ b/backend/src/users/users.controller.ts @@ -25,7 +25,7 @@ import { import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User as UserDecorator } from './users.decorator'; -import { User } from './interfaces/users.interface'; +import { IUser } from './interfaces/users.interface'; import { UsersService } from './users.service'; import { HttpExceptionFilter } from './exceptions/http-exception.filter'; import { ParseIntPipe } from './pipes/parse-int.pipe'; @@ -48,7 +48,7 @@ export class UsersController { @ApiCookieAuth('access_token') @ApiOkResponse({ description: 'Successfully returned all users' }) @ApiUnauthorizedResponse({ description: 'Unauthorized' }) - async findAll(): Promise { + async findAll(): Promise { return this.usersService.findAll(); } @@ -57,7 +57,7 @@ export class UsersController { @ApiCookieAuth('access_token') @ApiOkResponse({ description: 'Successfully returned user profile' }) @ApiUnauthorizedResponse({ description: 'Unauthorized' }) - async getProfile(@UserDecorator('user') user: User) { + async getProfile(@UserDecorator('user') user: IUser) { return this.usersService.getData(user); } @@ -66,7 +66,7 @@ export class UsersController { @ApiCookieAuth('access_token') @ApiOkResponse({ description: 'Successfully returned user' }) @ApiUnauthorizedResponse({ description: 'Unauthorized' }) - async findOne(@Param('id', new ParseIntPipe()) id: number): Promise { + async findOne(@Param('id', new ParseIntPipe()) id: number): Promise { return this.usersService.findOne(id); } @@ -75,7 +75,7 @@ export class UsersController { @ApiCookieAuth('access_token') @ApiOkResponse({ description: 'Successfully returned user by username' }) @ApiUnauthorizedResponse({ description: 'Unauthorized' }) - async findByUsername(@Param('username') username: string): Promise { + async findByUsername(@Param('username') username: string): Promise { return this.usersService.findByUsername(username); } diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index 89d948f3..812954ea 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Snippets } from '../entities/snippet.entity'; +import { Snippet } from '../entities/snippet.entity'; import { AuthService } from '../auth/auth.service'; -import { Users } from '../entities/user.entity'; +import { User } from '../entities/user.entity'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { UsersSubscriber } from './users.subscriber'; @@ -12,7 +12,7 @@ import { CheckUsername } from './validation/check-username'; import { CheckPassword } from './validation/check-password'; @Module({ - imports: [TypeOrmModule.forFeature([Users, Snippets])], + imports: [TypeOrmModule.forFeature([User, Snippet])], controllers: [UsersController], providers: [ UsersService, diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 04c35438..5ec3f2ba 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -10,48 +10,48 @@ import { MailerService } from '@nestjs-modules/mailer'; import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -import { Users } from '../entities/user.entity'; -import { Snippets } from '../entities/snippet.entity'; -import { User } from './interfaces/users.interface'; +import { User } from '../entities/user.entity'; +import { Snippet } from '../entities/snippet.entity'; +import { IUser } from './interfaces/users.interface'; import { RecoverUserDto } from './dto/recover-user.dto'; import { cipher, decipher } from './secure/cipher'; @Injectable() export class UsersService { constructor( - @InjectRepository(Users) - private usersRepository: Repository, - @InjectRepository(Snippets) - private snippetsRepository: Repository, + @InjectRepository(User) + private usersRepository: Repository, + @InjectRepository(Snippet) + private snippetsRepository: Repository, private readonly mailerService: MailerService, @InjectSentry() private readonly sentryService: SentryService, ) {} - async findOne(id: number): Promise { + async findOne(id: number): Promise { return this.usersRepository.findOneBy({ id }); } - async find(email: string): Promise { + async find(email: string): Promise { return this.usersRepository.findOneBy({ email }); } - async findByUsername(username: string): Promise { + async findByUsername(username: string): Promise { return this.usersRepository.findOneBy({ username: ILike(username) }); } - async findByEmail(email: string): Promise { + async findByEmail(email: string): Promise { return this.usersRepository.findOneBy({ email }); } - create(createUserDto: CreateUserDto): Promise { - const user = new Users(); + create(createUserDto: CreateUserDto): Promise { + const user = new User(); user.username = createUserDto.username; user.email = createUserDto.email.toLowerCase(); user.password = createUserDto.password; return this.usersRepository.save(user); } - async update(id: number, updateUserDto: UpdateUserDto): Promise { + async update(id: number, updateUserDto: UpdateUserDto): Promise { const { ...data } = updateUserDto; const currentUser = await this.usersRepository.findOneBy({ id }); const updatedUser = this.usersRepository.merge(currentUser, data); @@ -136,11 +136,11 @@ export class UsersService { await this.usersRepository.delete(id); } - findAll(): Promise { + findAll(): Promise { return this.usersRepository.find(); } - async getData({ id }: User): Promise { + async getData({ id }: IUser): Promise { const currentUser = await this.usersRepository.findOneBy({ id }); const snippets = await this.snippetsRepository.find({ relations: { diff --git a/backend/src/users/users.subscriber.ts b/backend/src/users/users.subscriber.ts index 65666204..6d425648 100644 --- a/backend/src/users/users.subscriber.ts +++ b/backend/src/users/users.subscriber.ts @@ -6,19 +6,19 @@ import { EventSubscriber, InsertEvent, } from 'typeorm'; -import { Users } from '../entities/user.entity'; +import { User } from '../entities/user.entity'; @EventSubscriber() -export class UsersSubscriber implements EntitySubscriberInterface { +export class UsersSubscriber implements EntitySubscriberInterface { constructor(dataSource: DataSource) { dataSource.subscribers.push(this); } listenTo() { - return Users; + return User; } - beforeInsert(event: InsertEvent): void | Promise { + beforeInsert(event: InsertEvent): void | Promise { console.log('BEFORE USER INSERTER: ', event.entity); } } diff --git a/backend/src/users/validation/validation.module.ts b/backend/src/users/validation/validation.module.ts index 362820c8..4ae1b460 100644 --- a/backend/src/users/validation/validation.module.ts +++ b/backend/src/users/validation/validation.module.ts @@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersService } from '../users.service'; import { UsersController } from '../users.controller'; -import { Users } from '../../entities/user.entity'; +import { User } from '../../entities/user.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Users])], + imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], })