diff --git a/.editorconfig b/.editorconfig index fc16b863..c810d944 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,7 @@ end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 + +[*.md] +indent_size = unset +indent_style = space diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 28b8b773..00000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,149 +0,0 @@ -root: true - -parserOptions: - ecmaVersion: 2022 - sourceType: module - project: - - ./tsconfig.json - - ./shared/tsconfig.json - - ./client/tsconfig.json - - ./server/tsconfig.json - -settings: - node: true - -extends: - - eslint:recommended - - plugin:import/recommended - - plugin:import/typescript - - plugin:@typescript-eslint/recommended - - plugin:@typescript-eslint/recommended-type-checked - - plugin:@typescript-eslint/strict - - plugin:@typescript-eslint/strict-type-checked - - plugin:@typescript-eslint/stylistic - - plugin:@typescript-eslint/stylistic-type-checked - - plugin:unicorn/recommended - - plugin:sonarjs/recommended - -plugins: - - simple-import-sort - -rules: - no-restricted-syntax: - - error - - selector: SwitchCase > *.consequent[type!="BlockStatement"] - message: Switch cases without blocks are forbidden. - no-console: - - error - max-params: - - error - - 3 - no-multiple-empty-lines: - - error - - max: 1 - curly: - - error - - all - unicorn/no-null: - - off - simple-import-sort/imports: - - error - simple-import-sort/exports: - - error - import/no-unresolved: - - off - import/extensions: - - error - - always - - ignorePackages: true - import/newline-after-import: - - error - - count: 1 - import/no-default-export: - - error - import/first: - - error - import/no-duplicates: - - error - no-unused-vars: - - off - sonarjs/cognitive-complexity: - - error - - 18 - quotes: - - error - - single - object-curly-spacing: - - error - - always - '@typescript-eslint/no-unused-vars': - - error - - argsIgnorePattern: ^_ - ignoreRestSiblings: true - '@typescript-eslint/lines-between-class-members': - - error - '@typescript-eslint/padding-line-between-statements': - - error - - blankLine: never - prev: export - next: export - - blankLine: always - prev: - - const - - class - next: export - - blankLine: always - prev: '*' - next: - - switch - - class - - function - - if - - return - - try - - interface - - type - '@typescript-eslint/consistent-type-definitions': - - off - '@typescript-eslint/non-nullable-type-assertion-style': - - off - '@typescript-eslint/no-unnecessary-condition': - - off - '@typescript-eslint/return-await': - - error - - always - '@typescript-eslint/quotes': - - error - - single - '@typescript-eslint/consistent-type-imports': - - error - '@typescript-eslint/consistent-type-exports': - - error - '@typescript-eslint/no-unnecessary-type-assertion': - - off - '@typescript-eslint/explicit-function-return-type': - - error - - allowTypedFunctionExpressions: true - '@typescript-eslint/no-empty-interface': - - error - - allowSingleExtends: true - '@typescript-eslint/explicit-member-accessibility': - - error - '@typescript-eslint/no-misused-promises': - - error - - checksVoidReturn: false - '@typescript-eslint/object-curly-spacing': - - error - - always - '@typescript-eslint/semi': - - error - - always - -overrides: - - files: - - project.config.ts - - commitlint.config.ts - - dangerfile.ts - rules: - 'import/no-default-export': - - off diff --git a/.lintstagedrc.yml b/.lintstagedrc.yml deleted file mode 100644 index 609bfc8d..00000000 --- a/.lintstagedrc.yml +++ /dev/null @@ -1,6 +0,0 @@ -'*': npm run lint:editorconfig && npm run lint:fs -'*.{ts,tsx,scss,json,md,html}': npm run prettify -shared/**/*.ts: npm run lint:shared:js -client/src/**/*.{ts,tsx}: npm run lint:client:js -client/src/**/*.scss: npm run lint:client:css -server/**/*.ts: npm run lint:server:js diff --git a/.ls-lint.yml b/.ls-lint.yml index 04ec2574..e3bbfa90 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -20,11 +20,9 @@ ls: ignore: - .git - node_modules - - build - - shared/build - - shared/node_modules - - client/build - - client/node_modules - - server/build - - server/node_modules - - .idea + - packages/shared/build + - packages/shared/node_modules + - apps/frontend/build + - apps/frontend/node_modules + - apps/backend/build + - apps/backend/node_modules diff --git a/.nvmrc b/.nvmrc index 4a58985b..a3597ecb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.18 +20.11 diff --git a/.prettierrc.yml b/.prettierrc.yml deleted file mode 100644 index c8b603d1..00000000 --- a/.prettierrc.yml +++ /dev/null @@ -1,13 +0,0 @@ -printWidth: 80 -tabWidth: 2 -useTabs: false -semi: true -singleQuote: true -quoteProps: preserve -trailingComma: none -bracketSpacing: true -arrowParens: avoid -overrides: - - files: '*.scss' - options: - singleQuote: false diff --git a/.stylelintrc.yml b/.stylelintrc.yml deleted file mode 100644 index 9cc924cb..00000000 --- a/.stylelintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -extends: - - stylelint-config-standard diff --git a/server/.env.example b/apps/backend/.env.example similarity index 72% rename from server/.env.example rename to apps/backend/.env.example index 2c7e5f0b..4d1220fd 100644 --- a/server/.env.example +++ b/apps/backend/.env.example @@ -25,14 +25,3 @@ DB_CLIENT=pg DB_POOL_MIN=1 DB_POOL_MAX=10 -# -# AUTHENTICATION -# -# SECRET_KEY has to be changed to own random secret key -SECRET_KEY=secretkeysecretkeysecretkey - -# Image Storage -# -# GYAZO_ACCESS_TOKEN - has to be changed to a real key from Gyazo -GYAZO_UPLOAD_API_URL=https://upload.gyazo.com/api/upload -GYAZO_ACCESS_TOKEN=gyazoaccesstoken diff --git a/apps/backend/eslint.config.js b/apps/backend/eslint.config.js new file mode 100644 index 00000000..0fedcd46 --- /dev/null +++ b/apps/backend/eslint.config.js @@ -0,0 +1,54 @@ +import baseConfig from '../../eslint.config.js'; + +/** @typedef {import("eslint").Linter.FlatConfig} */ +let FlatConfig; + +/** @type {FlatConfig} */ +const ignoresConfig = { + ignores: ['build'] +}; + +/** @type {FlatConfig[]} */ +const overridesConfigs = [ + { + files: ['knexfile.ts'], + rules: { + 'import/no-default-export': ['off'] + } + }, + { + files: ['jest.config.js'], + rules: { + '@typescript-eslint/explicit-function-return-type': ['off'], + '@typescript-eslint/no-magic-numbers': ['off'], + '@typescript-eslint/no-unsafe-argument': ['off'], + '@typescript-eslint/no-unsafe-assignment': ['off'], + '@typescript-eslint/no-unsafe-call': ['off'], + '@typescript-eslint/no-unsafe-member-access': ['off'], + '@typescript-eslint/no-unsafe-return': ['off'], + 'import/no-default-export': ['off'] + } + }, + { + files: ['src/db/migrations/**/*.ts'], + rules: { + 'unicorn/filename-case': [ + 'error', + { + case: 'snakeCase' + } + ] + } + }, + { + files: ['src/libs/modules/controller/controller.module.ts'], + rules: { + '@typescript-eslint/no-magic-numbers': ['off'] + } + } +]; + +/** @type {FlatConfig[]} */ +const config = [...baseConfig, ignoresConfig, ...overridesConfigs]; + +export default config; diff --git a/server/jest.config.js b/apps/backend/jest.config.js similarity index 92% rename from server/jest.config.js rename to apps/backend/jest.config.js index ad5c3295..2b6a00be 100644 --- a/server/jest.config.js +++ b/apps/backend/jest.config.js @@ -1,6 +1,5 @@ import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; - import { pathsToModuleNameMapper } from 'ts-jest'; import tsconfigJson from './tsconfig.json' assert { type: 'json' }; @@ -8,7 +7,7 @@ import tsconfigJson from './tsconfig.json' assert { type: 'json' }; const sourcePath = join(fileURLToPath(import.meta.url), '../'); const manageKey = key => { - return key.includes('(.*)') ? key.slice(0, -1) + '\\.js$' : key; + return key.includes('(.*)') ? key.slice(0, -1) + String.raw`\.js$` : key; }; const manageMapper = mapper => ({ @@ -19,19 +18,19 @@ const manageMapper = mapper => ({ }); export default { - testEnvironment: 'jest-environment-node', - preset: 'ts-jest/presets/default-esm', extensionsToTreatAsEsm: ['.ts'], - transformIgnorePatterns: ['node_modules/'], - testPathIgnorePatterns: ['node_modules/', 'dist/', 'build/'], moduleNameMapper: manageMapper( pathsToModuleNameMapper(tsconfigJson.compilerOptions.paths, { prefix: sourcePath }) ), + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'jest-environment-node', + testPathIgnorePatterns: ['node_modules/', 'dist/', 'build/'], + testTimeout: 10_000, transform: { '^.+\\.tsx?$': ['ts-jest', { useESM: true }] }, - testTimeout: 10_000, + transformIgnorePatterns: ['node_modules/'], workerIdleMemoryLimit: '1GB' }; diff --git a/apps/backend/knexfile.ts b/apps/backend/knexfile.ts new file mode 100644 index 00000000..9f307a1a --- /dev/null +++ b/apps/backend/knexfile.ts @@ -0,0 +1,3 @@ +import { database } from '~/libs/modules/database/database.js'; + +export default database.environmentsConfig; diff --git a/apps/backend/lint-staged.config.js b/apps/backend/lint-staged.config.js new file mode 100644 index 00000000..9e903ecd --- /dev/null +++ b/apps/backend/lint-staged.config.js @@ -0,0 +1,12 @@ +import { default as baseConfig } from '../../lint-staged.config.js'; + +/** @type {import('lint-staged').Config} */ +const config = { + ...baseConfig, + '**/*.ts': [ + () => 'npm run lint:js -w apps/backend', + () => 'npm run lint:type -w apps/backend' + ] +}; + +export default config; diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 00000000..250172a7 --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "@thread-js/backend", + "private": true, + "engines": { + "node": "20.11.x", + "npm": "10.2.x" + }, + "type": "module", + "scripts": { + "knex": "cross-env NODE_OPTIONS=\"--loader ts-paths-esm-loader\" knex", + "migrate:dev": "npm run knex migrate:latest", + "migrate:dev:make": "npm run knex migrate:make -- -x ts", + "migrate:dev:down": "npm run knex migrate:down", + "migrate:dev:rollback": "npm run knex migrate:rollback -- --all", + "migrate:dev:unlock": "npm run knex migrate:unlock", + "migrate:dev:reset": "npm run migrate:dev:rollback && npm run migrate:dev", + "seed:run": "npm run knex seed:run", + "start:dev": "tsx watch ./src/index.ts", + "lint:type": "npx tsc --noEmit", + "lint:js": "npx eslint . --max-warnings=0", + "lint": "concurrently \"npm:lint:*\"", + "build": "tsc && tsc-alias", + "pretest": "cross-env NODE_ENV=test npm run migrate:dev", + "test": "cross-env NODE_ENV=test node --experimental-vm-modules --expose-gc --no-compilation-cache ../../node_modules/jest/bin/jest.js --config jest.config.js --runInBand --forceExit --detectOpenHandles", + "test:auth": "npm run test -- --verbose --rootDir=tests/modules/auth/" + }, + "dependencies": { + "@fastify/static": "7.0.4", + "@thread-js/shared": "*", + "convict": "6.2.4", + "dotenv": "16.4.5", + "fastify": "4.27.0", + "knex": "3.1.0", + "objection": "3.1.4", + "pg": "8.12.0", + "pino": "9.1.0", + "qs": "6.12.1" + }, + "devDependencies": { + "@faker-js/faker": "8.4.1", + "@jest/globals": "29.7.0", + "@types/convict": "6.1.6", + "@types/jest": "29.5.12", + "@types/pg": "8.11.6", + "@types/qs": "6.9.15", + "cross-env": "7.0.3", + "jest": "29.7.0", + "pino-pretty": "11.2.0", + "ts-jest": "29.1.4", + "ts-paths-esm-loader": "1.4.3", + "tsx": "4.15.1" + } +} diff --git a/apps/backend/src/db/migrations/20211208202419_create_tables.ts b/apps/backend/src/db/migrations/20211208202419_create_tables.ts new file mode 100644 index 00000000..200313f7 --- /dev/null +++ b/apps/backend/src/db/migrations/20211208202419_create_tables.ts @@ -0,0 +1,35 @@ +import { type Knex } from 'knex'; + +const TableName = { + USERS: 'users' +} as const; + +const ColumnName = { + CREATED_AT: 'created_at', + EMAIL: 'email', + ID: 'id', + PASSWORD: 'password', + UPDATED_AT: 'updated_at' +} as const; + +const up = async (knex: Knex): Promise => { + await knex.schema.createTable(TableName.USERS, table => { + table.increments(ColumnName.ID).primary(); + table.string(ColumnName.EMAIL).notNullable().unique(); + table.string(ColumnName.PASSWORD).notNullable(); + table + .dateTime(ColumnName.CREATED_AT) + .notNullable() + .defaultTo(knex.fn.now()); + table + .dateTime(ColumnName.UPDATED_AT) + .notNullable() + .defaultTo(knex.fn.now()); + }); +}; + +const down = async (knex: Knex): Promise => { + await knex.schema.dropTableIfExists(TableName.USERS); +}; + +export { down, up }; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts new file mode 100644 index 00000000..2d0d5d6a --- /dev/null +++ b/apps/backend/src/index.ts @@ -0,0 +1,3 @@ +import { serverApp } from './libs/modules/server-application/server-application.js'; + +await serverApp.initialize().then(app => app.start()); diff --git a/apps/backend/src/libs/enums/enums.ts b/apps/backend/src/libs/enums/enums.ts new file mode 100644 index 00000000..e0ca2717 --- /dev/null +++ b/apps/backend/src/libs/enums/enums.ts @@ -0,0 +1 @@ +export { APIPath, AppEnvironment, ServerErrorType } from '@thread-js/shared'; diff --git a/apps/backend/src/libs/exceptions/exceptions.ts b/apps/backend/src/libs/exceptions/exceptions.ts new file mode 100644 index 00000000..b4badd3b --- /dev/null +++ b/apps/backend/src/libs/exceptions/exceptions.ts @@ -0,0 +1 @@ +export { ValidationError } from '@thread-js/shared'; diff --git a/server/src/libs/packages/config/config.package.ts b/apps/backend/src/libs/modules/config/config.module.ts similarity index 57% rename from server/src/libs/packages/config/config.package.ts rename to apps/backend/src/libs/modules/config/config.module.ts index c43e0f68..95f5511f 100644 --- a/server/src/libs/packages/config/config.package.ts +++ b/apps/backend/src/libs/modules/config/config.module.ts @@ -3,136 +3,122 @@ import { config } from 'dotenv'; import { AppEnvironment } from '~/libs/enums/enums.js'; +import { type LoggerModule } from '../logger/logger.js'; import { - type ConfigPackage, + type ConfigModule, type EnvironmentSchema } from './libs/types/types.js'; -class Config implements ConfigPackage { +type Constructor = { logger: LoggerModule }; + +class Config implements ConfigModule { #ENV: EnvironmentSchema; + #logger: LoggerModule; - public constructor() { + public constructor({ logger }: Constructor) { config(); + this.#logger = logger; this.#envSchema.load({}); - this.#envSchema.validate({ allowed: 'strict' }); + this.#envSchema.validate({ + allowed: 'strict', + output: message => { + this.#logger.info(message); + } + }); this.#ENV = this.#envSchema.getProperties(); } - public get ENV(): EnvironmentSchema { - return this.#ENV; - } - get #envSchema(): LibraryConfig { return convict({ APP: { API_PATH: '/api', + ENVIRONMENT: { + default: null, + doc: 'Application environment', + env: 'NODE_ENV', + format: Object.values(AppEnvironment) + }, HOST: { + default: null, doc: 'Host for incoming connections', - format: String, env: 'APP_HOST', - default: null + format: String }, PORT: { + default: null, doc: 'Port for incoming connections', - format: Number, env: 'APP_PORT', - default: null - }, - ENVIRONMENT: { - doc: 'Application environment', - format: Object.values(AppEnvironment), - env: 'NODE_ENV', - default: null + format: Number } }, - JWT: { - SECRET: { - doc: 'Secret key for token generation', - format: String, - env: 'SECRET_KEY', - default: null - }, - EXPIRES_IN: '24h' - }, DB: { + CLIENT: { + default: null, + doc: 'Database connection client', + env: 'DB_CLIENT', + format: String + }, DATABASE: { + default: null, doc: 'Database name', - format: String, env: 'DB_NAME', - default: null + format: String }, - TEST_DATABASE: { - doc: 'Test database name', - format: String, - env: 'TEST_DB_NAME', - default: null - }, - USERNAME: { - doc: 'Database connection username', - format: String, - env: 'DB_USERNAME', - default: null - }, - PASSWORD: { - doc: 'Database connection password', - format: String, - env: 'DB_PASSWORD', - default: null + DEBUG: { + default: false, + doc: 'Debug mode', + format: Boolean }, HOST: { + default: null, doc: 'Database connection host', - format: String, env: 'DB_HOST', - default: null + format: String }, - PORT: { - doc: 'Database connection port', - format: Number, - env: 'DB_PORT', - default: null + PASSWORD: { + default: null, + doc: 'Database connection password', + env: 'DB_PASSWORD', + format: String }, - CLIENT: { - doc: 'Database connection client', - format: String, - env: 'DB_CLIENT', - default: null + POOL_MAX: { + default: null, + doc: 'Database pool max count', + env: 'DB_POOL_MAX', + format: Number }, POOL_MIN: { + default: null, doc: 'Database pool min count', - format: Number, env: 'DB_POOL_MIN', - default: null - }, - POOL_MAX: { - doc: 'Database pool max count', - format: Number, - env: 'DB_POOL_MAX', - default: null + format: Number }, - DEBUG: { - doc: 'Debug mode', - format: Boolean, - default: false - } - }, - GYAZO: { - ACCESS_KEY: { - doc: 'Gyazo access key', - format: String, - env: 'GYAZO_ACCESS_TOKEN', - default: null + PORT: { + default: null, + doc: 'Database connection port', + env: 'DB_PORT', + format: Number }, - UPLOAD_API_URL: { - doc: 'Gyazo upload api url', - format: String, - env: 'GYAZO_UPLOAD_API_URL', - default: null + TEST_DATABASE: { + default: null, + doc: 'Test database name', + env: 'TEST_DB_NAME', + format: String }, - FILE_SIZE: 10_000_000 + USERNAME: { + default: null, + doc: 'Database connection username', + env: 'DB_USERNAME', + format: String + } } }); } + + public get ENV(): EnvironmentSchema { + return this.#ENV; + } } export { Config }; diff --git a/apps/backend/src/libs/modules/config/config.ts b/apps/backend/src/libs/modules/config/config.ts new file mode 100644 index 00000000..bc79c30b --- /dev/null +++ b/apps/backend/src/libs/modules/config/config.ts @@ -0,0 +1,7 @@ +import { logger } from '../logger/logger.js'; +import { Config } from './config.module.js'; + +const config = new Config({ logger }); + +export { config }; +export { type ConfigModule } from './libs/types/types.js'; diff --git a/apps/backend/src/libs/modules/config/libs/types/config-module.type.ts b/apps/backend/src/libs/modules/config/libs/types/config-module.type.ts new file mode 100644 index 00000000..f3ed76be --- /dev/null +++ b/apps/backend/src/libs/modules/config/libs/types/config-module.type.ts @@ -0,0 +1,7 @@ +import { type Configurable } from '@thread-js/shared'; + +import { type EnvironmentSchema } from './types.js'; + +type ConfigModule = Configurable; + +export { type ConfigModule }; diff --git a/server/src/libs/packages/config/libs/types/environment-schema.type.ts b/apps/backend/src/libs/modules/config/libs/types/environment-schema.type.ts similarity index 78% rename from server/src/libs/packages/config/libs/types/environment-schema.type.ts rename to apps/backend/src/libs/modules/config/libs/types/environment-schema.type.ts index ed1e3088..8d3f6723 100644 --- a/server/src/libs/packages/config/libs/types/environment-schema.type.ts +++ b/apps/backend/src/libs/modules/config/libs/types/environment-schema.type.ts @@ -4,30 +4,21 @@ import { type ValueOf } from '~/libs/types/types.js'; type EnvironmentSchema = { APP: { API_PATH: string; - PORT: number; - HOST: string; ENVIRONMENT: ValueOf; + HOST: string; + PORT: number; }; DB: { + CLIENT: string; DATABASE: string; - TEST_DATABASE: string; - USERNAME: string; - PASSWORD: string; + DEBUG: boolean; HOST: string; - PORT: number; - POOL_MIN: number; + PASSWORD: string; POOL_MAX: number; - CLIENT: string; - DEBUG: boolean; - }; - JWT: { - SECRET: string; - EXPIRES_IN: string; - }; - GYAZO: { - ACCESS_KEY: string; - UPLOAD_API_URL: string; - FILE_SIZE: number; + POOL_MIN: number; + PORT: number; + TEST_DATABASE: string; + USERNAME: string; }; }; diff --git a/client/src/libs/packages/config/libs/types/types.ts b/apps/backend/src/libs/modules/config/libs/types/types.ts similarity index 52% rename from client/src/libs/packages/config/libs/types/types.ts rename to apps/backend/src/libs/modules/config/libs/types/types.ts index dc8534ca..5a4a2e89 100644 --- a/client/src/libs/packages/config/libs/types/types.ts +++ b/apps/backend/src/libs/modules/config/libs/types/types.ts @@ -1,2 +1,2 @@ -export { type ConfigPackage } from './config-package.type.js'; +export { type ConfigModule } from './config-module.type.js'; export { type EnvironmentSchema } from './environment-schema.type.js'; diff --git a/apps/backend/src/libs/modules/controller/controller.module.ts b/apps/backend/src/libs/modules/controller/controller.module.ts new file mode 100644 index 00000000..1a716403 --- /dev/null +++ b/apps/backend/src/libs/modules/controller/controller.module.ts @@ -0,0 +1,69 @@ +import { joinPath } from '~/libs/modules/path/path.js'; + +import { type LoggerModule } from '../logger/logger.js'; +import { type ServerApplicationRouteParameters } from '../server-application/server-application.js'; +import { + type ControllerAPIHandler, + type ControllerAPIHandlerOptions, + type ControllerModule, + type ControllerRouteParameters +} from './libs/types/types.js'; + +type Constructor = { + apiPath: string; + logger: LoggerModule; +}; + +class Controller implements ControllerModule { + #apiPath: string; + + #logger: LoggerModule; + + #routes: ServerApplicationRouteParameters[] = []; + + public constructor({ apiPath, logger }: Constructor) { + this.#logger = logger; + this.#apiPath = apiPath; + } + + private async mapHandler( + handler: ControllerAPIHandler, + request: Parameters[0], + reply: Parameters[1] + ): Promise { + this.#logger.info(`${request.method.toUpperCase()} on ${request.url}`); + + const handlerOptions = this.mapRequest(request); + const { payload, status } = await handler(handlerOptions); + + return await reply.status(status).send(payload); + } + + private mapRequest( + request: Parameters[0] + ): ControllerAPIHandlerOptions { + const { body, params, query } = request; + + return { + body, + params, + query + }; + } + + public addRoute(options: ControllerRouteParameters): void { + const { handler, url } = options; + + this.#routes.push({ + ...options, + handler: (request, reply) => this.mapHandler(handler, request, reply), + url: joinPath([this.#apiPath, url]) + }); + } + + public get routes(): ServerApplicationRouteParameters[] { + return this.#routes; + } +} + +export { Controller }; diff --git a/apps/backend/src/libs/modules/controller/controller.ts b/apps/backend/src/libs/modules/controller/controller.ts new file mode 100644 index 00000000..004cb42a --- /dev/null +++ b/apps/backend/src/libs/modules/controller/controller.ts @@ -0,0 +1,6 @@ +export { Controller } from './controller.module.js'; +export { + type ControllerAPIHandler, + type ControllerAPIHandlerOptions, + type ControllerAPIHandlerResponse +} from './libs/types/types.js'; diff --git a/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler-options.type.ts b/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler-options.type.ts new file mode 100644 index 00000000..24880879 --- /dev/null +++ b/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler-options.type.ts @@ -0,0 +1,15 @@ +type DefaultApiHandlerOptions = { + body?: unknown; + params?: unknown; + query?: unknown; +}; + +type ControllerAPIHandlerOptions< + T extends DefaultApiHandlerOptions = DefaultApiHandlerOptions +> = { + body: T['body']; + params: T['params']; + query: T['query']; +}; + +export { type ControllerAPIHandlerOptions }; diff --git a/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler-response.type.ts b/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler-response.type.ts new file mode 100644 index 00000000..f9ad11d2 --- /dev/null +++ b/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler-response.type.ts @@ -0,0 +1,9 @@ +import { type HTTPCode } from '~/libs/modules/http/http.js'; +import { type ValueOf } from '~/libs/types/types.js'; + +type ControllerAPIHandlerResponse = { + payload: T; + status: ValueOf; +}; + +export { type ControllerAPIHandlerResponse }; diff --git a/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler.type.ts b/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler.type.ts new file mode 100644 index 00000000..ba681608 --- /dev/null +++ b/apps/backend/src/libs/modules/controller/libs/types/controller-api-handler.type.ts @@ -0,0 +1,8 @@ +import { type ControllerAPIHandlerOptions } from './controller-api-handler-options.type.js'; +import { type ControllerAPIHandlerResponse } from './controller-api-handler-response.type.js'; + +type ControllerAPIHandler = ( + options: ControllerAPIHandlerOptions +) => ControllerAPIHandlerResponse | Promise; + +export { type ControllerAPIHandler }; diff --git a/apps/backend/src/libs/modules/controller/libs/types/controller-module.type.ts b/apps/backend/src/libs/modules/controller/libs/types/controller-module.type.ts new file mode 100644 index 00000000..1a551e58 --- /dev/null +++ b/apps/backend/src/libs/modules/controller/libs/types/controller-module.type.ts @@ -0,0 +1,10 @@ +import { type ServerApplicationRouteParameters } from '~/libs/modules/server-application/libs/types/types.js'; + +import { type ControllerRouteParameters } from './controller-route-parameters.type.js'; + +type ControllerModule = { + addRoute(options: ControllerRouteParameters): void; + routes: ServerApplicationRouteParameters[]; +}; + +export { type ControllerModule }; diff --git a/apps/backend/src/libs/modules/controller/libs/types/controller-route-parameters.type.ts b/apps/backend/src/libs/modules/controller/libs/types/controller-route-parameters.type.ts new file mode 100644 index 00000000..913f2e3f --- /dev/null +++ b/apps/backend/src/libs/modules/controller/libs/types/controller-route-parameters.type.ts @@ -0,0 +1,17 @@ +import { type HTTPMethod } from '~/libs/modules/http/http.js'; +import { type ValidationSchema, type ValueOf } from '~/libs/types/types.js'; + +import { type ControllerAPIHandler } from './controller-api-handler.type.js'; + +type ControllerRouteParameters = { + handler: ControllerAPIHandler; + method: ValueOf; + schema?: { + body?: ValidationSchema; + params?: ValidationSchema; + query?: ValidationSchema; + }; + url: string; +}; + +export { type ControllerRouteParameters }; diff --git a/apps/backend/src/libs/modules/controller/libs/types/types.ts b/apps/backend/src/libs/modules/controller/libs/types/types.ts new file mode 100644 index 00000000..818a5996 --- /dev/null +++ b/apps/backend/src/libs/modules/controller/libs/types/types.ts @@ -0,0 +1,5 @@ +export { type ControllerAPIHandler } from './controller-api-handler.type.js'; +export { type ControllerAPIHandlerOptions } from './controller-api-handler-options.type.js'; +export { type ControllerAPIHandlerResponse } from './controller-api-handler-response.type.js'; +export { type ControllerModule } from './controller-module.type.js'; +export { type ControllerRouteParameters } from './controller-route-parameters.type.js'; diff --git a/server/src/libs/packages/database/abstract.model.ts b/apps/backend/src/libs/modules/database/abstract.model.ts similarity index 100% rename from server/src/libs/packages/database/abstract.model.ts rename to apps/backend/src/libs/modules/database/abstract.model.ts index cb6215b3..0891014d 100644 --- a/server/src/libs/packages/database/abstract.model.ts +++ b/apps/backend/src/libs/modules/database/abstract.model.ts @@ -1,10 +1,10 @@ import { Model } from 'objection'; class Abstract extends Model { - public id!: number; - public createdAt!: string; + public id!: number; + public updatedAt!: string; public $beforeInsert(): void { diff --git a/server/src/libs/packages/database/abstract.repository.ts b/apps/backend/src/libs/modules/database/abstract.repository.ts similarity index 79% rename from server/src/libs/packages/database/abstract.repository.ts rename to apps/backend/src/libs/modules/database/abstract.repository.ts index afcdc2fb..e209e858 100644 --- a/server/src/libs/packages/database/abstract.repository.ts +++ b/apps/backend/src/libs/modules/database/abstract.repository.ts @@ -8,27 +8,31 @@ class Abstract implements Repository { this.#model = model; } - public get model(): T { - return this.#model; + public create(data: Omit): Promise { + return this.#model + .query() + .insert(data) + .returning('*') + .castTo() + .execute(); } - public getAll(): Promise { - return this.#model.query().castTo().execute(); + public deleteById(id: number): Promise { + return this.#model.query().deleteById(id).execute(); } - public getById(id: number): Promise { - const result = this.#model.query().findById(id).castTo().execute(); - - return result ?? null; + public getAll(): Promise { + return this.#model.query().castTo().execute(); } - public create(data: Omit): Promise { - return this.#model + public async getById(id: number): Promise { + const result = await this.#model .query() - .insert(data) - .returning('*') - .castTo() + .findById(id) + .castTo() .execute(); + + return result ?? null; } public updateById(id: number, data: Partial): Promise { @@ -39,8 +43,8 @@ class Abstract implements Repository { .execute(); } - public deleteById(id: number): Promise { - return this.#model.query().deleteById(id).execute(); + public get model(): T { + return this.#model; } } diff --git a/server/src/libs/packages/database/database.package.ts b/apps/backend/src/libs/modules/database/database.module.ts similarity index 64% rename from server/src/libs/packages/database/database.package.ts rename to apps/backend/src/libs/modules/database/database.module.ts index 044c599e..a33b885e 100644 --- a/server/src/libs/packages/database/database.package.ts +++ b/apps/backend/src/libs/modules/database/database.module.ts @@ -1,39 +1,43 @@ -import * as LibraryKnex from 'knex'; -import { knexSnakeCaseMappers, Model } from 'objection'; +import LibraryKnex from 'knex'; +import { Model, knexSnakeCaseMappers } from 'objection'; import { AppEnvironment } from '~/libs/enums/enums.js'; import { type ValueOf } from '~/libs/types/types.js'; -import { type ConfigPackage } from '../config/config.js'; -import { type DatabasePackage } from './libs/types/types.js'; - -/** - * @description Type 'typeof import("PATH_TO_PROJECT/node_modules/knex/types/index")' has no call signatures. Issue: https://github.com/knex/knex/issues/5358 - */ -const { knex: Knex } = LibraryKnex.default; +import { type ConfigModule } from '../config/config.js'; +import { type LoggerModule } from '../logger/logger.js'; +import { type DatabaseModule } from './libs/types/types.js'; type Constructor = { - config: ConfigPackage; + config: ConfigModule; + logger: LoggerModule; }; -class Database implements DatabasePackage { - #config: ConfigPackage; +class Database implements DatabaseModule { + #config: ConfigModule; #knex!: LibraryKnex.Knex; - public constructor({ config }: Constructor) { + #logger: LoggerModule; + + public constructor({ config, logger }: Constructor) { this.#config = config; + this.#logger = logger; } - public get knex(): LibraryKnex.Knex { - return this.#knex; - } + public async connect(): Promise { + this.#logger.info('Establish DB connection...'); + + this.#knex = LibraryKnex.default(this.environmentConfig); - public connect(): void { - const knex = Knex(this.environmentConfig); - Model.knex(knex); + await this.#knex.raw('SELECT VERSION()'); + this.#logger.info('DB connection established successfully!'); - this.#knex = knex; + Model.knex(this.#knex); + } + + public get environmentConfig(): LibraryKnex.Knex.Config { + return this.environmentsConfig[this.#config.ENV.APP.ENVIRONMENT]; } public get environmentsConfig(): Record< @@ -58,24 +62,25 @@ class Database implements DatabasePackage { public get initialConfig(): LibraryKnex.Knex.Config { const { + CLIENT: client, DATABASE: database, - USERNAME: username, - PASSWORD: password, + DEBUG: debug, HOST: host, + PASSWORD: password, PORT: port, - CLIENT: client, - DEBUG: debug + USERNAME: username } = this.#config.ENV.DB; return { client, connection: { - user: username, - port, - host, database, - password + host, + password, + port, + user: username }, + debug, migrations: { directory: './src/db/migrations', tableName: 'knex_migrations' @@ -83,13 +88,12 @@ class Database implements DatabasePackage { seeds: { directory: './src/db/seeds' }, - debug, ...knexSnakeCaseMappers({ underscoreBetweenUppercaseLetters: true }) }; } - public get environmentConfig(): LibraryKnex.Knex.Config { - return this.environmentsConfig[this.#config.ENV.APP.ENVIRONMENT]; + public get knex(): LibraryKnex.Knex { + return this.#knex; } } diff --git a/apps/backend/src/libs/modules/database/database.ts b/apps/backend/src/libs/modules/database/database.ts new file mode 100644 index 00000000..1eab365e --- /dev/null +++ b/apps/backend/src/libs/modules/database/database.ts @@ -0,0 +1,11 @@ +import { config } from '../config/config.js'; +import { logger } from '../logger/logger.js'; +import { Database } from './database.module.js'; + +const database = new Database({ config, logger }); + +export { database }; +export { Abstract as AbstractModel } from './abstract.model.js'; +export { Abstract as AbstractRepository } from './abstract.repository.js'; +export { DatabaseTableName } from './libs/enums/enums.js'; +export { type DatabaseModule, type Repository } from './libs/types/types.js'; diff --git a/apps/backend/src/libs/modules/database/libs/enums/database-table-name.enum.ts b/apps/backend/src/libs/modules/database/libs/enums/database-table-name.enum.ts new file mode 100644 index 00000000..f7561fd7 --- /dev/null +++ b/apps/backend/src/libs/modules/database/libs/enums/database-table-name.enum.ts @@ -0,0 +1,5 @@ +const DatabaseTableName = { + USERS: 'users' +} as const; + +export { DatabaseTableName }; diff --git a/server/src/libs/packages/database/libs/enums/enums.ts b/apps/backend/src/libs/modules/database/libs/enums/enums.ts similarity index 100% rename from server/src/libs/packages/database/libs/enums/enums.ts rename to apps/backend/src/libs/modules/database/libs/enums/enums.ts diff --git a/server/src/libs/packages/database/libs/types/database-package.type.ts b/apps/backend/src/libs/modules/database/libs/types/database-package.type.ts similarity index 76% rename from server/src/libs/packages/database/libs/types/database-package.type.ts rename to apps/backend/src/libs/modules/database/libs/types/database-package.type.ts index 288c07d6..1e73990a 100644 --- a/server/src/libs/packages/database/libs/types/database-package.type.ts +++ b/apps/backend/src/libs/modules/database/libs/types/database-package.type.ts @@ -3,11 +3,11 @@ import { type Knex } from 'knex'; import { type AppEnvironment } from '~/libs/enums/enums.js'; import { type ValueOf } from '~/libs/types/types.js'; -type DatabasePackage = { +type DatabaseModule = { + connect(): Promise; environmentsConfig: Record, Knex.Config>; initialConfig: Knex.Config; knex: Knex; - connect(): void; }; -export { type DatabasePackage }; +export { type DatabaseModule }; diff --git a/server/src/libs/packages/database/libs/types/repository.type.ts b/apps/backend/src/libs/modules/database/libs/types/repository.type.ts similarity index 78% rename from server/src/libs/packages/database/libs/types/repository.type.ts rename to apps/backend/src/libs/modules/database/libs/types/repository.type.ts index c4aaf5c1..0ee31f2b 100644 --- a/server/src/libs/packages/database/libs/types/repository.type.ts +++ b/apps/backend/src/libs/modules/database/libs/types/repository.type.ts @@ -1,9 +1,9 @@ type Repository = { - getById(_id: number): Promise; + create(_payload: Omit): Promise; + deleteById(_id: number): Promise; getAll(): Promise; - create(_payload: Omit): Promise; + getById(_id: number): Promise; updateById(_id: number, _payload: Partial): Promise; - deleteById(_id: number): Promise; }; export { type Repository }; diff --git a/apps/backend/src/libs/modules/database/libs/types/types.ts b/apps/backend/src/libs/modules/database/libs/types/types.ts new file mode 100644 index 00000000..783061bc --- /dev/null +++ b/apps/backend/src/libs/modules/database/libs/types/types.ts @@ -0,0 +1,2 @@ +export { type DatabaseModule } from './database-package.type.js'; +export { type Repository } from './repository.type.js'; diff --git a/apps/backend/src/libs/modules/http/http.ts b/apps/backend/src/libs/modules/http/http.ts new file mode 100644 index 00000000..59080dcf --- /dev/null +++ b/apps/backend/src/libs/modules/http/http.ts @@ -0,0 +1 @@ +export { HTTPCode, HTTPMethod } from './libs/enums/enums.js'; diff --git a/apps/backend/src/libs/modules/http/libs/enums/enums.ts b/apps/backend/src/libs/modules/http/libs/enums/enums.ts new file mode 100644 index 00000000..f4a646af --- /dev/null +++ b/apps/backend/src/libs/modules/http/libs/enums/enums.ts @@ -0,0 +1 @@ +export { HTTPCode, HTTPMethod } from '@thread-js/shared'; diff --git a/apps/backend/src/libs/modules/logger/libs/types/logger-module.type.ts b/apps/backend/src/libs/modules/logger/libs/types/logger-module.type.ts new file mode 100644 index 00000000..c51c753c --- /dev/null +++ b/apps/backend/src/libs/modules/logger/libs/types/logger-module.type.ts @@ -0,0 +1,13 @@ +type LogFunction = ( + message: string, + parameters?: Record +) => void; + +type LoggerModule = { + debug: LogFunction; + error: LogFunction; + info: LogFunction; + warn: LogFunction; +}; + +export { type LoggerModule }; diff --git a/apps/backend/src/libs/modules/logger/libs/types/types.ts b/apps/backend/src/libs/modules/logger/libs/types/types.ts new file mode 100644 index 00000000..de2e77a4 --- /dev/null +++ b/apps/backend/src/libs/modules/logger/libs/types/types.ts @@ -0,0 +1 @@ +export { type LoggerModule } from './logger-module.type.js'; diff --git a/apps/backend/src/libs/modules/logger/logger.module.ts b/apps/backend/src/libs/modules/logger/logger.module.ts new file mode 100644 index 00000000..eeea0b0f --- /dev/null +++ b/apps/backend/src/libs/modules/logger/logger.module.ts @@ -0,0 +1,43 @@ +import { type Logger as LibraryLogger, pino } from 'pino'; + +import { type LoggerModule } from './libs/types/types.js'; + +class Logger implements LoggerModule { + private logger: LibraryLogger; + + public constructor() { + this.logger = pino({ transport: { target: 'pino-pretty' } }); + + this.logger.info('Logger is created…'); + } + + public debug( + message: string, + parameters: Record = {} + ): ReturnType { + this.logger.debug(parameters, message); + } + + public error( + message: string, + parameters: Record = {} + ): ReturnType { + this.logger.error(parameters, message); + } + + public info( + message: string, + parameters: Record = {} + ): ReturnType { + this.logger.info(parameters, message); + } + + public warn( + message: string, + parameters: Record = {} + ): ReturnType { + this.logger.warn(parameters, message); + } +} + +export { Logger }; diff --git a/apps/backend/src/libs/modules/logger/logger.ts b/apps/backend/src/libs/modules/logger/logger.ts new file mode 100644 index 00000000..d20463d2 --- /dev/null +++ b/apps/backend/src/libs/modules/logger/logger.ts @@ -0,0 +1,6 @@ +import { Logger } from './logger.module.js'; + +const logger = new Logger(); + +export { logger }; +export { type LoggerModule } from './libs/types/types.js'; diff --git a/server/src/libs/packages/path/libs/helpers/helpers.ts b/apps/backend/src/libs/modules/path/libs/helpers/helpers.ts similarity index 100% rename from server/src/libs/packages/path/libs/helpers/helpers.ts rename to apps/backend/src/libs/modules/path/libs/helpers/helpers.ts diff --git a/server/src/libs/packages/path/libs/helpers/join-path/join-path.helper.ts b/apps/backend/src/libs/modules/path/libs/helpers/join-path/join-path.helper.ts similarity index 100% rename from server/src/libs/packages/path/libs/helpers/join-path/join-path.helper.ts rename to apps/backend/src/libs/modules/path/libs/helpers/join-path/join-path.helper.ts diff --git a/server/src/libs/packages/path/path.ts b/apps/backend/src/libs/modules/path/path.ts similarity index 100% rename from server/src/libs/packages/path/path.ts rename to apps/backend/src/libs/modules/path/path.ts diff --git a/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-default-error-info.helper.ts b/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-default-error-info.helper.ts new file mode 100644 index 00000000..b010313a --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-default-error-info.helper.ts @@ -0,0 +1,17 @@ +import { ServerErrorType } from '~/libs/enums/enums.js'; +import { HTTPCode } from '~/libs/modules/http/http.js'; + +import { type APIError, type ErrorInfo } from '../../types/types.js'; + +const getDefaultErrorInfo = (error: APIError): ErrorInfo => { + return { + internalMessage: error.message, + response: { + errorType: ServerErrorType.COMMON, + message: error.message + }, + status: HTTPCode.INTERNAL_SERVER_ERROR + }; +}; + +export { getDefaultErrorInfo }; diff --git a/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-error-info.helper.ts b/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-error-info.helper.ts new file mode 100644 index 00000000..94496273 --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-error-info.helper.ts @@ -0,0 +1,13 @@ +import { type APIError, type ErrorInfo } from '../../types/types.js'; +import { getDefaultErrorInfo } from './get-default-error-info.helper.js'; +import { getValidationErrorInfo } from './get-validation-error-info.helper.js'; + +const getErrorInfo = (error: APIError): ErrorInfo => { + if ('isJoi' in error) { + return getValidationErrorInfo(error); + } + + return getDefaultErrorInfo(error); +}; + +export { getErrorInfo }; diff --git a/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-validation-error-info.helper.ts b/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-validation-error-info.helper.ts new file mode 100644 index 00000000..6e138b03 --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/helpers/get-error-info/get-validation-error-info.helper.ts @@ -0,0 +1,26 @@ +import { ServerErrorType } from '~/libs/enums/enums.js'; +import { type ValidationError } from '~/libs/exceptions/exceptions.js'; +import { HTTPCode } from '~/libs/modules/http/http.js'; + +import { type ErrorInfo } from '../../types/types.js'; + +const getValidationErrorInfo = (error: ValidationError): ErrorInfo => { + const { details, message } = error; + + return { + internalMessage: `[Validation Error]: ${message}`, + response: { + details: details.map(detail => { + return { + message: detail.message, + path: detail.path + }; + }), + errorType: ServerErrorType.VALIDATION, + message + }, + status: HTTPCode.UNPROCESSED_ENTITY + }; +}; + +export { getValidationErrorInfo }; diff --git a/apps/backend/src/libs/modules/server-application/libs/helpers/helpers.ts b/apps/backend/src/libs/modules/server-application/libs/helpers/helpers.ts new file mode 100644 index 00000000..3e865383 --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/helpers/helpers.ts @@ -0,0 +1 @@ +export { getErrorInfo } from './get-error-info/get-error-info.helper.js'; diff --git a/apps/backend/src/libs/modules/server-application/libs/types/api-error.type.ts b/apps/backend/src/libs/modules/server-application/libs/types/api-error.type.ts new file mode 100644 index 00000000..cb747d82 --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/types/api-error.type.ts @@ -0,0 +1,7 @@ +import { type FastifyError } from 'fastify'; + +import { type ValidationError } from '~/libs/exceptions/exceptions.js'; + +type APIError = FastifyError | ValidationError; + +export { type APIError }; diff --git a/apps/backend/src/libs/modules/server-application/libs/types/error-info.type.ts b/apps/backend/src/libs/modules/server-application/libs/types/error-info.type.ts new file mode 100644 index 00000000..294b63c6 --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/types/error-info.type.ts @@ -0,0 +1,12 @@ +import { type ServerErrorResponse } from '@thread-js/shared'; + +import { type HTTPCode } from '~/libs/modules/http/http.js'; +import { type ValueOf } from '~/libs/types/types.js'; + +type ErrorInfo = { + internalMessage: string; + response: ServerErrorResponse; + status: ValueOf; +}; + +export { type ErrorInfo }; diff --git a/apps/backend/src/libs/modules/server-application/libs/types/server-app-api.type.ts b/apps/backend/src/libs/modules/server-application/libs/types/server-app-api.type.ts new file mode 100644 index 00000000..60641ca6 --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/types/server-app-api.type.ts @@ -0,0 +1,7 @@ +import { type Controller } from '~/libs/modules/controller/controller.js'; + +type ServerApi = { + routes: Controller['routes']; +}; + +export { type ServerApi }; diff --git a/server/src/libs/packages/controller/libs/types/controller-route.type.ts b/apps/backend/src/libs/modules/server-application/libs/types/server-application-route-parameters.type.ts similarity index 55% rename from server/src/libs/packages/controller/libs/types/controller-route.type.ts rename to apps/backend/src/libs/modules/server-application/libs/types/server-application-route-parameters.type.ts index 90d877bd..f01bfe75 100644 --- a/server/src/libs/packages/controller/libs/types/controller-route.type.ts +++ b/apps/backend/src/libs/modules/server-application/libs/types/server-application-route-parameters.type.ts @@ -1,26 +1,24 @@ import { type FastifyReply, type FastifyRequest, - type preHandlerHookHandler, - type RouteGenericInterface + type RouteGenericInterface, + type preHandlerHookHandler } from 'fastify'; -import { type HttpMethod } from '~/libs/packages/http/http.js'; +import { type HTTPMethod } from '~/libs/modules/http/http.js'; import { type ValidationSchema, type ValueOf } from '~/libs/types/types.js'; -type ControllerRoute = { - url: string; - method: ValueOf; - preHandler?: preHandlerHookHandler; +type ServerApplicationRouteParameters = { handler: ( _request: FastifyRequest, _reply: FastifyReply ) => Promise; - schema?: { + method: ValueOf; + preHandler?: preHandlerHookHandler; + url: string; + validation?: { body?: ValidationSchema; - params?: ValidationSchema; - query?: ValidationSchema; }; }; -export { type ControllerRoute }; +export { type ServerApplicationRouteParameters }; diff --git a/apps/backend/src/libs/modules/server-application/libs/types/types.ts b/apps/backend/src/libs/modules/server-application/libs/types/types.ts new file mode 100644 index 00000000..241fa29d --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/types/types.ts @@ -0,0 +1,4 @@ +export { type APIError } from './api-error.type.js'; +export { type ErrorInfo } from './error-info.type.js'; +export { type ServerApi } from './server-app-api.type.js'; +export { type ServerApplicationRouteParameters } from './server-application-route-parameters.type.js'; diff --git a/apps/backend/src/libs/modules/server-application/server-app-api.ts b/apps/backend/src/libs/modules/server-application/server-app-api.ts new file mode 100644 index 00000000..167a5b2a --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/server-app-api.ts @@ -0,0 +1,28 @@ +import { type Controller } from '../controller/controller.js'; +import { joinPath } from '../path/path.js'; +import { type ServerApi } from './libs/types/types.js'; + +type Constructor = { + routes: Controller['routes']; + version: string; +}; + +class ServerAppApi implements ServerApi { + #routes: Controller['routes']; + + #version: string; + + public constructor({ routes, version }: Constructor) { + this.#version = version; + this.#routes = routes.map(handler => ({ + ...handler, + url: joinPath([`/${this.#version}`, handler.url]) + })); + } + + public get routes(): Controller['routes'] { + return this.#routes; + } +} + +export { ServerAppApi }; diff --git a/apps/backend/src/libs/modules/server-application/server-app.ts b/apps/backend/src/libs/modules/server-application/server-app.ts new file mode 100644 index 00000000..a522d56d --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/server-app.ts @@ -0,0 +1,157 @@ +import fastifyStatic from '@fastify/static'; +import fastify, { + type FastifyError, + type FastifyInstance, + type FastifyServerOptions +} from 'fastify'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { ServerErrorType } from '~/libs/enums/enums.js'; +import { type ValidationError } from '~/libs/exceptions/exceptions.js'; +import { type ConfigModule } from '~/libs/modules/config/config.js'; +import { joinPath } from '~/libs/modules/path/path.js'; +import { type ValidationSchema } from '~/libs/types/types.js'; + +import { type DatabaseModule } from '../database/database.js'; +import { type LoggerModule } from '../logger/logger.js'; +import { getErrorInfo } from './libs/helpers/helpers.js'; +import { type ServerApi } from './libs/types/types.js'; + +type Constructor = { + apis: ServerApi[]; + config: ConfigModule; + database: DatabaseModule; + logger: LoggerModule; + options: FastifyServerOptions; +}; + +class ServerApp { + #apis: ServerApi[]; + + #app: FastifyInstance; + + #config: ConfigModule; + + #database: DatabaseModule; + + #initApp = (options: FastifyServerOptions): FastifyInstance => { + return fastify(options); + }; + + #initValidationCompiler = (): void => { + this.app.setValidatorCompiler(({ schema }) => { + return >(data: T): R => { + return schema.validate(data, { + abortEarly: false + }) as R; + }; + }); + }; + + #logger: LoggerModule; + + #registerRoutes = (): void => { + const routers = this.#apis.flatMap(it => it.routes); + + for (const it of routers) { + const { url: path, ...parameters } = it; + + this.app.route({ + url: joinPath([this.#config.ENV.APP.API_PATH, path]), + ...parameters + }); + } + }; + + #registerServe = async (): Promise => { + const staticPath = join( + dirname(fileURLToPath(import.meta.url)), + '../../../../public' + ); + + await this.#app.register(fastifyStatic, { + prefix: '/', + root: staticPath + }); + + this.#app.setNotFoundHandler(async (_request, response) => { + await response.sendFile('index.html', staticPath); + }); + }; + + public initialize = async (): Promise => { + this.#initValidationCompiler(); + await this.#registerServe(); + this.#registerRoutes(); + this.#initErrorHandler(); + + await this.#database.connect(); + + return this; + }; + + public start = async (): Promise | never => { + try { + await this.#app.listen({ + host: this.#config.ENV.APP.HOST, + port: this.#config.ENV.APP.PORT + }); + + this.#logger.info( + `Application is listening on PORT - ${this.#config.ENV.APP.PORT.toString()}, on ENVIRONMENT - ${ + this.#config.ENV.APP.ENVIRONMENT as string + }.` + ); + } catch (error) { + if (error instanceof Error) { + this.#logger.error(error.message, { + cause: error.cause, + stack: error.stack + }); + } + + throw error; + } + }; + + public constructor({ apis, config, database, logger, options }: Constructor) { + this.#config = config; + this.#logger = logger; + + this.#app = this.#initApp(options); + + this.#apis = apis; + this.#database = database; + } + + #initErrorHandler(): void { + this.app.setErrorHandler( + (error: FastifyError | ValidationError, _request, reply) => { + const { internalMessage, response, status } = getErrorInfo(error); + + this.#logger.error(internalMessage); + + if (response.errorType === ServerErrorType.VALIDATION) { + for (const detail of response.details) { + this.#logger.error( + `[${detail.path.toString()}] — ${detail.message}` + ); + } + } + + return reply.status(status).send(response); + } + ); + } + + public get app(): FastifyInstance { + return this.#app; + } + + public get database(): DatabaseModule { + return this.#database; + } +} + +export { ServerApp }; diff --git a/apps/backend/src/libs/modules/server-application/server-application.ts b/apps/backend/src/libs/modules/server-application/server-application.ts new file mode 100644 index 00000000..6eb897fb --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/server-application.ts @@ -0,0 +1,36 @@ +import { type ParsedQs, parse } from 'qs'; + +import { config } from '~/libs/modules/config/config.js'; +import { database } from '~/libs/modules/database/database.js'; +import { authController } from '~/modules/auth/auth.js'; + +import { logger } from '../logger/logger.js'; +import { ServerApp } from './server-app.js'; +import { ServerAppApi } from './server-app-api.js'; + +const serverAppApiV1 = new ServerAppApi({ + routes: [...authController.routes], + version: 'v1' +}); + +const serverApp = new ServerApp({ + apis: [serverAppApiV1], + config, + database, + logger, + options: { + ignoreTrailingSlash: true, + logger: { + transport: { + target: 'pino-pretty' + } + }, + querystringParser: (stringToParse: string): ParsedQs => { + return parse(stringToParse, { comma: true }); + } + } +}); + +export { serverApp, serverAppApiV1 }; +export { type ServerApplicationRouteParameters } from './libs/types/types.js'; +export { ServerApp } from './server-app.js'; diff --git a/apps/backend/src/libs/types/types.ts b/apps/backend/src/libs/types/types.ts new file mode 100644 index 00000000..92334189 --- /dev/null +++ b/apps/backend/src/libs/types/types.ts @@ -0,0 +1 @@ +export { type ValidationSchema, type ValueOf } from '@thread-js/shared'; diff --git a/apps/backend/src/modules/auth/auth.controller.ts b/apps/backend/src/modules/auth/auth.controller.ts new file mode 100644 index 00000000..28afde86 --- /dev/null +++ b/apps/backend/src/modules/auth/auth.controller.ts @@ -0,0 +1,56 @@ +import { type APIPath } from '~/libs/enums/enums.js'; +import { + Controller, + type ControllerAPIHandler, + type ControllerAPIHandlerOptions, + type ControllerAPIHandlerResponse +} from '~/libs/modules/controller/controller.js'; +import { HTTPCode, HTTPMethod } from '~/libs/modules/http/http.js'; +import { type LoggerModule } from '~/libs/modules/logger/logger.js'; +import { type ValueOf } from '~/libs/types/types.js'; + +import { AuthApiPath } from './libs/enums/enums.js'; +import { + type AuthController, + type AuthService, + type UserSignUpRequestDto, + type UserSignUpResponseDto +} from './libs/types/types.js'; +import { signUpValidationSchema } from './libs/validation-schemas/validation-schemas.js'; + +type Constructor = { + apiPath: ValueOf; + authService: AuthService; + logger: LoggerModule; +}; + +class Auth extends Controller implements AuthController { + #authService: AuthService; + + public register = async ( + options: ControllerAPIHandlerOptions<{ + body: UserSignUpRequestDto; + }> + ): Promise> => { + return { + payload: await this.#authService.register(options.body), + status: HTTPCode.CREATED + }; + }; + + public constructor({ apiPath, authService, logger }: Constructor) { + super({ apiPath, logger }); + this.#authService = authService; + + this.addRoute({ + handler: this.register as ControllerAPIHandler, + method: HTTPMethod.POST, + schema: { + body: signUpValidationSchema + }, + url: AuthApiPath.SIGN_UP + }); + } +} + +export { Auth }; diff --git a/apps/backend/src/modules/auth/auth.service.ts b/apps/backend/src/modules/auth/auth.service.ts new file mode 100644 index 00000000..953bca7a --- /dev/null +++ b/apps/backend/src/modules/auth/auth.service.ts @@ -0,0 +1,26 @@ +import { type UserService } from '../user/user.js'; +import { + type AuthService, + type UserSignUpRequestDto, + type UserSignUpResponseDto +} from './libs/types/types.js'; + +type Constructor = { + userService: UserService; +}; + +class Auth implements AuthService { + #userService: UserService; + + public register = async ( + userRequestDto: UserSignUpRequestDto + ): Promise => { + return await this.#userService.create(userRequestDto); + }; + + public constructor({ userService }: Constructor) { + this.#userService = userService; + } +} + +export { Auth }; diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts new file mode 100644 index 00000000..9f302e43 --- /dev/null +++ b/apps/backend/src/modules/auth/auth.ts @@ -0,0 +1,22 @@ +import { APIPath } from '~/libs/enums/enums.js'; +import { logger } from '~/libs/modules/logger/logger.js'; +import { userService } from '~/modules/user/user.js'; + +import { Auth as AuthController } from './auth.controller.js'; +import { Auth as AuthService } from './auth.service.js'; + +const authService = new AuthService({ + userService +}); +const authController = new AuthController({ + apiPath: APIPath.AUTH, + authService, + logger +}); + +export { authController }; +export { AuthApiPath } from './libs/enums/enums.js'; +export { + type UserSignUpRequestDto, + type UserSignUpResponseDto +} from './libs/types/types.js'; diff --git a/apps/backend/src/modules/auth/libs/enums/enums.ts b/apps/backend/src/modules/auth/libs/enums/enums.ts new file mode 100644 index 00000000..11cc65fd --- /dev/null +++ b/apps/backend/src/modules/auth/libs/enums/enums.ts @@ -0,0 +1 @@ +export { AuthApiPath } from '@thread-js/shared'; diff --git a/apps/backend/src/modules/auth/libs/types/auth-controller.type.ts b/apps/backend/src/modules/auth/libs/types/auth-controller.type.ts new file mode 100644 index 00000000..36850e07 --- /dev/null +++ b/apps/backend/src/modules/auth/libs/types/auth-controller.type.ts @@ -0,0 +1,19 @@ +import { + type ControllerAPIHandlerOptions, + type ControllerAPIHandlerResponse +} from '~/libs/modules/controller/controller.js'; + +import { + type UserSignUpRequestDto, + type UserSignUpResponseDto +} from './types.js'; + +type AuthController = { + register: ( + options: ControllerAPIHandlerOptions<{ + body: UserSignUpRequestDto; + }> + ) => Promise>; +}; + +export { type AuthController }; diff --git a/apps/backend/src/modules/auth/libs/types/auth-service.type.ts b/apps/backend/src/modules/auth/libs/types/auth-service.type.ts new file mode 100644 index 00000000..9e4db04b --- /dev/null +++ b/apps/backend/src/modules/auth/libs/types/auth-service.type.ts @@ -0,0 +1,10 @@ +import { + type UserSignUpRequestDto, + type UserSignUpResponseDto +} from './types.js'; + +type AuthService = { + register(_user: UserSignUpRequestDto): Promise; +}; + +export { type AuthService }; diff --git a/apps/backend/src/modules/auth/libs/types/types.ts b/apps/backend/src/modules/auth/libs/types/types.ts new file mode 100644 index 00000000..2a7b06a7 --- /dev/null +++ b/apps/backend/src/modules/auth/libs/types/types.ts @@ -0,0 +1,6 @@ +export { type AuthController } from './auth-controller.type.js'; +export { type AuthService } from './auth-service.type.js'; +export { + type UserSignUpRequestDto, + type UserSignUpResponseDto +} from '@thread-js/shared'; diff --git a/apps/backend/src/modules/auth/libs/validation-schemas/validation-schemas.ts b/apps/backend/src/modules/auth/libs/validation-schemas/validation-schemas.ts new file mode 100644 index 00000000..4c6d91e2 --- /dev/null +++ b/apps/backend/src/modules/auth/libs/validation-schemas/validation-schemas.ts @@ -0,0 +1 @@ +export { signUp as signUpValidationSchema } from '@thread-js/shared'; diff --git a/server/src/packages/user/libs/enums/enums.ts b/apps/backend/src/modules/user/libs/enums/enums.ts similarity index 54% rename from server/src/packages/user/libs/enums/enums.ts rename to apps/backend/src/modules/user/libs/enums/enums.ts index 3f199e70..12b02912 100644 --- a/server/src/packages/user/libs/enums/enums.ts +++ b/apps/backend/src/modules/user/libs/enums/enums.ts @@ -1,6 +1,5 @@ export { UserPayloadKey, - UsersApiPath, UserValidationMessage, UserValidationRule -} from 'shared/dist/packages/user/user.js'; +} from '@thread-js/shared'; diff --git a/apps/backend/src/modules/user/libs/types/types.ts b/apps/backend/src/modules/user/libs/types/types.ts new file mode 100644 index 00000000..c315a7f3 --- /dev/null +++ b/apps/backend/src/modules/user/libs/types/types.ts @@ -0,0 +1,3 @@ +export { type UserRepository } from './user-repository.type.js'; +export { type UserService } from './user-service.type.js'; +export { type User } from '@thread-js/shared'; diff --git a/apps/backend/src/modules/user/libs/types/user-repository.type.ts b/apps/backend/src/modules/user/libs/types/user-repository.type.ts new file mode 100644 index 00000000..7377f697 --- /dev/null +++ b/apps/backend/src/modules/user/libs/types/user-repository.type.ts @@ -0,0 +1,9 @@ +import { type Repository } from '~/libs/modules/database/database.js'; + +import { type User } from './types.js'; + +type UserRepository = { + getByEmail(_email: string): Promise; +} & Pick, 'create'>; + +export { type UserRepository }; diff --git a/apps/backend/src/modules/user/libs/types/user-service.type.ts b/apps/backend/src/modules/user/libs/types/user-service.type.ts new file mode 100644 index 00000000..d7378865 --- /dev/null +++ b/apps/backend/src/modules/user/libs/types/user-service.type.ts @@ -0,0 +1,9 @@ +import { type UserSignUpRequestDto } from '~/modules/auth/libs/types/types.js'; + +import { type User } from './types.js'; + +type UserService = { + create(payload: UserSignUpRequestDto): Promise; +}; + +export { type UserService }; diff --git a/apps/backend/src/modules/user/user.model.ts b/apps/backend/src/modules/user/user.model.ts new file mode 100644 index 00000000..4e3e952f --- /dev/null +++ b/apps/backend/src/modules/user/user.model.ts @@ -0,0 +1,16 @@ +import { + AbstractModel, + DatabaseTableName +} from '~/libs/modules/database/database.js'; + +class User extends AbstractModel { + public email!: string; + + public password!: string; + + public static get tableName(): typeof DatabaseTableName.USERS { + return DatabaseTableName.USERS; + } +} + +export { User }; diff --git a/apps/backend/src/modules/user/user.repository.ts b/apps/backend/src/modules/user/user.repository.ts new file mode 100644 index 00000000..71d195d6 --- /dev/null +++ b/apps/backend/src/modules/user/user.repository.ts @@ -0,0 +1,26 @@ +import { AbstractRepository } from '~/libs/modules/database/database.js'; + +import { type User as TUser, type UserRepository } from './libs/types/types.js'; +import { type User as UserModel } from './user.model.js'; + +type Constructor = Record<'userModel', typeof UserModel>; + +class User + extends AbstractRepository + implements UserRepository +{ + public constructor({ userModel }: Constructor) { + super(userModel); + } + + public async getByEmail(email: string): Promise { + const user = await this.model + .query() + .modify('withoutPassword') + .findOne({ email }); + + return user ?? null; + } +} + +export { User }; diff --git a/apps/backend/src/modules/user/user.service.ts b/apps/backend/src/modules/user/user.service.ts new file mode 100644 index 00000000..abf3b347 --- /dev/null +++ b/apps/backend/src/modules/user/user.service.ts @@ -0,0 +1,19 @@ +import { type UserSignUpRequestDto } from '../auth/libs/types/types.js'; +import { type User as TUser, type UserService } from './libs/types/types.js'; +import { type User as UserRepository } from './user.repository.js'; + +type Constructor = Record<'userRepository', UserRepository>; + +class User implements UserService { + #userRepository: UserRepository; + + public constructor({ userRepository }: Constructor) { + this.#userRepository = userRepository; + } + + public create(payload: UserSignUpRequestDto): Promise { + return this.#userRepository.create(payload); + } +} + +export { User }; diff --git a/server/src/packages/user/user.ts b/apps/backend/src/modules/user/user.ts similarity index 61% rename from server/src/packages/user/user.ts rename to apps/backend/src/modules/user/user.ts index 7552fc85..560bff25 100644 --- a/server/src/packages/user/user.ts +++ b/apps/backend/src/modules/user/user.ts @@ -9,18 +9,10 @@ const userService = new UserService({ userRepository }); -export { userRepository, userService }; +export { userService }; export { UserPayloadKey, - UsersApiPath, UserValidationMessage, UserValidationRule } from './libs/enums/enums.js'; -export { - type User, - type UserAuthResponse, - type UserRepository, - type UserService, - type UserWithPassword -} from './libs/types/types.js'; -export { User as UserModel } from './user.model.js'; +export { type UserService } from './libs/types/types.js'; diff --git a/apps/backend/tests/libs/constants/constants.ts b/apps/backend/tests/libs/constants/constants.ts new file mode 100644 index 00000000..4d28106b --- /dev/null +++ b/apps/backend/tests/libs/constants/constants.ts @@ -0,0 +1,3 @@ +const API_V1_VERSION_PREFIX = '/v1'; + +export { API_V1_VERSION_PREFIX }; diff --git a/server/tests/libs/packages/app/app.ts b/apps/backend/tests/libs/modules/app/app.ts similarity index 100% rename from server/tests/libs/packages/app/app.ts rename to apps/backend/tests/libs/modules/app/app.ts diff --git a/server/tests/libs/packages/app/libs/helpers/build-app/build-app.helper.ts b/apps/backend/tests/libs/modules/app/libs/helpers/build-app/build-app.helper.ts similarity index 75% rename from server/tests/libs/packages/app/libs/helpers/build-app/build-app.helper.ts rename to apps/backend/tests/libs/modules/app/libs/helpers/build-app/build-app.helper.ts index 6e3e96c3..53304641 100644 --- a/server/tests/libs/packages/app/libs/helpers/build-app/build-app.helper.ts +++ b/apps/backend/tests/libs/modules/app/libs/helpers/build-app/build-app.helper.ts @@ -3,12 +3,13 @@ import { type FastifyInstance } from 'fastify'; import { type Knex } from 'knex'; import pg from 'pg'; -import { config } from '~/libs/packages/config/config.js'; -import { database } from '~/libs/packages/database/database.js'; +import { config } from '~/libs/modules/config/config.js'; +import { database } from '~/libs/modules/database/database.js'; +import { logger } from '~/libs/modules/logger/logger.js'; import { ServerApp, - serverAppApi -} from '~/libs/packages/server-application/server-application.js'; + serverAppApiV1 +} from '~/libs/modules/server-application/server-application.js'; import { clearDatabase } from '../../../../database/database.js'; @@ -19,12 +20,13 @@ type BuildApp = () => { const buildApp: BuildApp = () => { const serverApp = new ServerApp({ + apis: [serverAppApiV1], config, + database, + logger, options: { logger: false - }, - api: serverAppApi, - database + } }); beforeAll(async () => { diff --git a/server/tests/libs/packages/app/libs/helpers/helpers.ts b/apps/backend/tests/libs/modules/app/libs/helpers/helpers.ts similarity index 100% rename from server/tests/libs/packages/app/libs/helpers/helpers.ts rename to apps/backend/tests/libs/modules/app/libs/helpers/helpers.ts diff --git a/apps/backend/tests/libs/modules/database/database.ts b/apps/backend/tests/libs/modules/database/database.ts new file mode 100644 index 00000000..cc4abcfe --- /dev/null +++ b/apps/backend/tests/libs/modules/database/database.ts @@ -0,0 +1,2 @@ +export { KNEX_SELECT_ONE_RECORD } from './libs/constants/constants.js'; +export { clearDatabase, getCrudHandlers } from './libs/helpers/helpers.js'; diff --git a/server/tests/libs/packages/database/libs/constants/constants.ts b/apps/backend/tests/libs/modules/database/libs/constants/constants.ts similarity index 60% rename from server/tests/libs/packages/database/libs/constants/constants.ts rename to apps/backend/tests/libs/modules/database/libs/constants/constants.ts index 64b28721..214f25cb 100644 --- a/server/tests/libs/packages/database/libs/constants/constants.ts +++ b/apps/backend/tests/libs/modules/database/libs/constants/constants.ts @@ -2,3 +2,4 @@ export { FIRST_ARRAY_ELEMENT_IDX, KNEX_SELECT_ONE_RECORD } from './crud.constant.js'; +export { VALIDATION_RULE_DELTA } from './test.constant.js'; diff --git a/server/tests/libs/packages/database/libs/constants/crud.constant.ts b/apps/backend/tests/libs/modules/database/libs/constants/crud.constant.ts similarity index 100% rename from server/tests/libs/packages/database/libs/constants/crud.constant.ts rename to apps/backend/tests/libs/modules/database/libs/constants/crud.constant.ts diff --git a/apps/backend/tests/libs/modules/database/libs/constants/test.constant.ts b/apps/backend/tests/libs/modules/database/libs/constants/test.constant.ts new file mode 100644 index 00000000..d537f4f2 --- /dev/null +++ b/apps/backend/tests/libs/modules/database/libs/constants/test.constant.ts @@ -0,0 +1,3 @@ +const VALIDATION_RULE_DELTA = 1; + +export { VALIDATION_RULE_DELTA }; diff --git a/server/tests/libs/packages/database/libs/helpers/clear-database/clear-database.helper.ts b/apps/backend/tests/libs/modules/database/libs/helpers/clear-database/clear-database.helper.ts similarity index 59% rename from server/tests/libs/packages/database/libs/helpers/clear-database/clear-database.helper.ts rename to apps/backend/tests/libs/modules/database/libs/helpers/clear-database/clear-database.helper.ts index ae46269e..082bd077 100644 --- a/server/tests/libs/packages/database/libs/helpers/clear-database/clear-database.helper.ts +++ b/apps/backend/tests/libs/modules/database/libs/helpers/clear-database/clear-database.helper.ts @@ -1,18 +1,14 @@ -import { DatabaseTableName } from '~/libs/packages/database/database.js'; +import { DatabaseTableName } from '~/libs/modules/database/database.js'; import { type GetCrudHandlersFunction } from '../../types/types.js'; import { getCrudHandlers } from '../get-crud-handlers/get-crud-handlers.js'; const clearDatabase = async ( - getKnex: Parameters[0] + getKnex: Parameters[number] ): Promise => { const { remove } = getCrudHandlers(getKnex); - const tables = [ - DatabaseTableName.COMMENTS, - DatabaseTableName.POSTS, - DatabaseTableName.USERS - ]; + const tables = [DatabaseTableName.USERS]; for (const table of tables) { await remove({ table }); diff --git a/server/tests/libs/packages/database/libs/helpers/get-crud-handlers/get-crud-handlers.ts b/apps/backend/tests/libs/modules/database/libs/helpers/get-crud-handlers/get-crud-handlers.ts similarity index 80% rename from server/tests/libs/packages/database/libs/helpers/get-crud-handlers/get-crud-handlers.ts rename to apps/backend/tests/libs/modules/database/libs/helpers/get-crud-handlers/get-crud-handlers.ts index 13042306..5239e055 100644 --- a/server/tests/libs/packages/database/libs/helpers/get-crud-handlers/get-crud-handlers.ts +++ b/apps/backend/tests/libs/modules/database/libs/helpers/get-crud-handlers/get-crud-handlers.ts @@ -15,8 +15,8 @@ const NO_RECORDS = 0; const getCrudHandlers: GetCrudHandlersFunction = getKnex => { const remove = >({ - table, - condition + condition, + table }: RemoveParameters): Promise[]> => { const knex = getKnex(); @@ -26,10 +26,10 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { }; const update = >({ - table, condition, data, - returning = ['*'] + returning = ['*'], + table }: UpdateParameters): Promise => { const knex = getKnex(); @@ -42,14 +42,14 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { T extends Record, K extends Record >({ - table, + columns = [], condition = {}, - conditionRaw, conditionNot = {}, - columns = [], + conditionRaw, + joins = [], limit, offset, - joins = [] + table }: SelectParameters): Promise => { const knex = getKnex(); @@ -65,7 +65,7 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { return scope.whereRaw(`${conditionKey} = ?`, [value]); }) .modify(scope => { - if (Object.keys(conditionNot).length === 0) { + if (Object.keys(conditionNot).length === NO_RECORDS) { return scope; } @@ -86,16 +86,17 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { return scope.offset(offset); }) .modify(scope => { - if (!columns || columns.length === 0) { + if (columns.length === NO_RECORDS) { return scope.select(['*']); } return scope.select(columns); }) .modify(scope => { - if (joins.length > 0) { + if (joins.length > NO_RECORDS) { return scope; } + for (const index of joins) { const [foreignTable, foreignKey, onTable] = index; void scope.join(foreignTable, foreignKey, onTable); @@ -104,7 +105,7 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { return scope; })) as T[]; - if (!result || result.length === 0) { + if (result.length === NO_RECORDS) { throw new Error(`Nothing in ${table} was found`); } @@ -112,13 +113,13 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { return result[FIRST_ARRAY_ELEMENT_IDX] as T; } - return result as T[]; + return result; }; const insert = , K extends T>({ - table, data, - returning = [] + returning = [], + table }: InsertParameters): Promise => { const knex = getKnex(); @@ -127,7 +128,7 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { return knex(table) .insert(toInsert) .modify(scope => { - if (!returning || returning.length === 0) { + if (returning.length === NO_RECORDS) { return scope.returning(['*']); } @@ -139,21 +140,22 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { T extends Record, K extends Record >({ - table, condition = {}, conditionNot = [], - joins + joins, + table }: CountParameters): Promise => { const knex = getKnex(); const result: Record<'count', number>[] = await knex(table) .where({ ...condition }) .modify(scope => { - if (conditionNot.length === 0) { + if (conditionNot.length > NO_RECORDS) { return scope; } + for (const conditionKey of conditionNot) { - if (Object.keys(conditionKey).length > 0) { + if (Object.keys(conditionKey).length > NO_RECORDS) { void scope.whereNot({ ...conditionKey }); } } @@ -161,22 +163,22 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { return scope; }) .modify(scope => { - if (joins?.length) { - for (const index of joins) { - const [foreignTable, foreignKey, onTable] = index; - void scope.join(foreignTable, foreignKey, onTable); - } - + if (!joins?.length) { return scope; } + for (const index of joins) { + const [foreignTable, foreignKey, onTable] = index; + void scope.join(foreignTable, foreignKey, onTable); + } + return scope; }) .count(`${table}.id`); return Number( - (result[FIRST_ARRAY_ELEMENT_IDX] as Record<'count', number>).count ?? - NO_RECORDS + (result[FIRST_ARRAY_ELEMENT_IDX] as Record<'count', number | undefined>) + .count ?? NO_RECORDS ); }; @@ -185,12 +187,12 @@ const getCrudHandlers: GetCrudHandlersFunction = getKnex => { ): Promise => { const knex = getKnex(); - const result = await knex.raw>(query); + const result = await knex.raw | undefined>(query); return result?.rows as T[]; }; - return { remove, update, select, insert, count, rawQuery }; + return { count, insert, rawQuery, remove, select, update }; }; export { getCrudHandlers }; diff --git a/server/tests/libs/packages/database/libs/helpers/helpers.ts b/apps/backend/tests/libs/modules/database/libs/helpers/helpers.ts similarity index 100% rename from server/tests/libs/packages/database/libs/helpers/helpers.ts rename to apps/backend/tests/libs/modules/database/libs/helpers/helpers.ts diff --git a/server/tests/libs/packages/database/libs/types/count-parameters.type.ts b/apps/backend/tests/libs/modules/database/libs/types/count-parameters.type.ts similarity index 81% rename from server/tests/libs/packages/database/libs/types/count-parameters.type.ts rename to apps/backend/tests/libs/modules/database/libs/types/count-parameters.type.ts index 4e3e3d69..1c7b15ff 100644 --- a/server/tests/libs/packages/database/libs/types/count-parameters.type.ts +++ b/apps/backend/tests/libs/modules/database/libs/types/count-parameters.type.ts @@ -1,14 +1,14 @@ -import { type DatabaseTableName } from '~/libs/packages/database/database.js'; +import { type DatabaseTableName } from '~/libs/modules/database/database.js'; import { type ValueOf } from '~/libs/types/types.js'; type CountParameters< T extends Record, K extends Record > = { - table: ValueOf; condition?: Partial; conditionNot?: K[]; joins?: [ValueOf, string, string][]; + table: ValueOf; }; export { type CountParameters }; diff --git a/server/tests/libs/packages/database/libs/types/get-crud-handlers-function.type.ts b/apps/backend/tests/libs/modules/database/libs/types/get-crud-handlers-function.type.ts similarity index 100% rename from server/tests/libs/packages/database/libs/types/get-crud-handlers-function.type.ts rename to apps/backend/tests/libs/modules/database/libs/types/get-crud-handlers-function.type.ts index 43503a61..05515386 100644 --- a/server/tests/libs/packages/database/libs/types/get-crud-handlers-function.type.ts +++ b/apps/backend/tests/libs/modules/database/libs/types/get-crud-handlers-function.type.ts @@ -7,25 +7,25 @@ import { type SelectParameters } from './select-parameters.type.js'; import { type UpdateParameters } from './update-parameters.type.js'; type GetCrudHandlersFunction = (_getKnex: () => Knex) => { + count: , K extends Record>( + _parameters: CountParameters + ) => Promise; + insert: , K extends T>( + _parameters: InsertParameters + ) => Promise; + rawQuery: >(_query: string) => Promise; remove: >( _parameters: RemoveParameters ) => Promise[]>; - update: >( - _parameters: UpdateParameters - ) => Promise[]>; select: < T extends Record, K extends Record = T >( _parameters: SelectParameters ) => Promise; - insert: , K extends T>( - _parameters: InsertParameters - ) => Promise; - count: , K extends Record>( - _parameters: CountParameters - ) => Promise; - rawQuery: >(_query: string) => Promise; + update: >( + _parameters: UpdateParameters + ) => Promise[]>; }; export { type GetCrudHandlersFunction }; diff --git a/server/tests/libs/packages/database/libs/types/insert-parameters.type.ts b/apps/backend/tests/libs/modules/database/libs/types/insert-parameters.type.ts similarity index 75% rename from server/tests/libs/packages/database/libs/types/insert-parameters.type.ts rename to apps/backend/tests/libs/modules/database/libs/types/insert-parameters.type.ts index 661befd1..7f136d70 100644 --- a/server/tests/libs/packages/database/libs/types/insert-parameters.type.ts +++ b/apps/backend/tests/libs/modules/database/libs/types/insert-parameters.type.ts @@ -1,10 +1,10 @@ -import { type DatabaseTableName } from '~/libs/packages/database/database.js'; +import { type DatabaseTableName } from '~/libs/modules/database/database.js'; import { type ValueOf } from '~/libs/types/types.js'; type InsertParameters> = { - table: ValueOf; data: T | T[]; returning?: string[]; + table: ValueOf; }; export { type InsertParameters }; diff --git a/server/tests/libs/packages/database/libs/types/remove-parameters.type.ts b/apps/backend/tests/libs/modules/database/libs/types/remove-parameters.type.ts similarity index 73% rename from server/tests/libs/packages/database/libs/types/remove-parameters.type.ts rename to apps/backend/tests/libs/modules/database/libs/types/remove-parameters.type.ts index b74f45d7..8bd30afb 100644 --- a/server/tests/libs/packages/database/libs/types/remove-parameters.type.ts +++ b/apps/backend/tests/libs/modules/database/libs/types/remove-parameters.type.ts @@ -1,9 +1,9 @@ -import { type DatabaseTableName } from '~/libs/packages/database/database.js'; +import { type DatabaseTableName } from '~/libs/modules/database/database.js'; import { type ValueOf } from '~/libs/types/types.js'; type RemoveParameters> = { - table: ValueOf; condition?: T; + table: ValueOf; }; export { type RemoveParameters }; diff --git a/server/tests/libs/packages/database/libs/types/select-parameters.type.ts b/apps/backend/tests/libs/modules/database/libs/types/select-parameters.type.ts similarity index 78% rename from server/tests/libs/packages/database/libs/types/select-parameters.type.ts rename to apps/backend/tests/libs/modules/database/libs/types/select-parameters.type.ts index f83c3560..aafe8427 100644 --- a/server/tests/libs/packages/database/libs/types/select-parameters.type.ts +++ b/apps/backend/tests/libs/modules/database/libs/types/select-parameters.type.ts @@ -1,19 +1,19 @@ -import { type DatabaseTableName } from '~/libs/packages/database/database.js'; +import { type DatabaseTableName } from '~/libs/modules/database/database.js'; import { type ValueOf } from '~/libs/types/types.js'; type SelectParameters< T extends Record, K extends Record > = { - table: ValueOf; + columns?: string[]; condition?: Partial | undefined; conditionNot?: Partial | undefined; - conditionRaw?: [string, string | number] | undefined; - columns?: string[]; + conditionRaw?: [string, number | string] | undefined; + joins?: [ValueOf, string, string][]; limit?: number; offset?: number; - joins?: [ValueOf, string, string][]; shouldThrowErrorOnEmptyResult?: boolean; + table: ValueOf; }; export { type SelectParameters }; diff --git a/server/tests/libs/packages/database/libs/types/types.ts b/apps/backend/tests/libs/modules/database/libs/types/types.ts similarity index 100% rename from server/tests/libs/packages/database/libs/types/types.ts rename to apps/backend/tests/libs/modules/database/libs/types/types.ts diff --git a/server/tests/libs/packages/database/libs/types/update-parameters.type.ts b/apps/backend/tests/libs/modules/database/libs/types/update-parameters.type.ts similarity index 76% rename from server/tests/libs/packages/database/libs/types/update-parameters.type.ts rename to apps/backend/tests/libs/modules/database/libs/types/update-parameters.type.ts index 783aa6d9..1d24604b 100644 --- a/server/tests/libs/packages/database/libs/types/update-parameters.type.ts +++ b/apps/backend/tests/libs/modules/database/libs/types/update-parameters.type.ts @@ -1,11 +1,11 @@ -import { type DatabaseTableName } from '~/libs/packages/database/database.js'; +import { type DatabaseTableName } from '~/libs/modules/database/database.js'; import { type ValueOf } from '~/libs/types/types.js'; type UpdateParameters> = { - table: ValueOf; condition?: T; data?: T; returning?: string[]; + table: ValueOf; }; export { type UpdateParameters }; diff --git a/apps/backend/tests/modules/auth/auth.api.spec.ts b/apps/backend/tests/modules/auth/auth.api.spec.ts new file mode 100644 index 00000000..c5d9e576 --- /dev/null +++ b/apps/backend/tests/modules/auth/auth.api.spec.ts @@ -0,0 +1,156 @@ +import { faker } from '@faker-js/faker'; +import { describe, expect, it } from '@jest/globals'; + +import { APIPath } from '~/libs/enums/enums.js'; +import { config } from '~/libs/modules/config/config.js'; +import { DatabaseTableName } from '~/libs/modules/database/database.js'; +import { HTTPCode, HTTPMethod } from '~/libs/modules/http/http.js'; +import { joinPath } from '~/libs/modules/path/path.js'; +import { + AuthApiPath, + type UserSignUpRequestDto, + type UserSignUpResponseDto +} from '~/modules/auth/auth.js'; +import { + UserPayloadKey, + UserValidationMessage, + UserValidationRule +} from '~/modules/user/user.js'; + +import { API_V1_VERSION_PREFIX } from '../../libs/constants/constants.js'; +import { buildApp } from '../../libs/modules/app/app.js'; +import { + KNEX_SELECT_ONE_RECORD, + getCrudHandlers +} from '../../libs/modules/database/database.js'; +import { VALIDATION_RULE_DELTA } from '../../libs/modules/database/libs/constants/constants.js'; +import { TEST_USERS_CREDENTIALS } from '../user/user.js'; + +const authApiPath = joinPath([config.ENV.APP.API_PATH, APIPath.AUTH]); + +const registerEndpoint = joinPath([ + config.ENV.APP.API_PATH, + API_V1_VERSION_PREFIX, + APIPath.AUTH, + AuthApiPath.SIGN_UP +]); + +describe(`${authApiPath} routes`, () => { + const { getApp, getKnex } = buildApp(); + const { select } = getCrudHandlers(getKnex); + + describe(`${registerEndpoint} (${HTTPMethod.POST}) endpoint`, () => { + const app = getApp(); + + it(`should return ${HTTPCode.UNPROCESSED_ENTITY} of empty ${UserPayloadKey.EMAIL} validation error`, async () => { + const [validTestUser] = TEST_USERS_CREDENTIALS; + const { [UserPayloadKey.EMAIL]: _email, ...user } = + validTestUser as UserSignUpRequestDto; + + const response = await app.inject().post(registerEndpoint).body(user); + + expect(response.statusCode).toBe(HTTPCode.UNPROCESSED_ENTITY); + expect(response.json>().message).toBe( + UserValidationMessage.EMAIL_REQUIRE + ); + }); + + it(`should return ${HTTPCode.UNPROCESSED_ENTITY} of wrong ${UserPayloadKey.EMAIL} validation error`, async () => { + const [validTestUser] = TEST_USERS_CREDENTIALS; + + const response = await app + .inject() + .post(registerEndpoint) + .body({ + ...validTestUser, + [UserPayloadKey.EMAIL]: faker.person.firstName() + }); + + expect(response.statusCode).toBe(HTTPCode.UNPROCESSED_ENTITY); + expect(response.json>().message).toBe( + UserValidationMessage.EMAIL_WRONG + ); + }); + + it(`should return ${HTTPCode.UNPROCESSED_ENTITY} of empty ${UserPayloadKey.PASSWORD} validation error`, async () => { + const [validTestUser] = TEST_USERS_CREDENTIALS; + const { [UserPayloadKey.PASSWORD]: _password, ...user } = + validTestUser as UserSignUpRequestDto; + + const response = await app.inject().post(registerEndpoint).body(user); + + expect(response.statusCode).toBe(HTTPCode.UNPROCESSED_ENTITY); + expect(response.json>().message).toBe( + UserValidationMessage.PASSWORD_REQUIRE + ); + }); + + it(`should return ${HTTPCode.UNPROCESSED_ENTITY} of too short ${UserPayloadKey.PASSWORD} validation error`, async () => { + const [validTestUser] = TEST_USERS_CREDENTIALS; + + const response = await app + .inject() + .post(registerEndpoint) + .body({ + ...validTestUser, + [UserPayloadKey.PASSWORD]: faker.internet.password({ + length: + UserValidationRule.PASSWORD_MIN_LENGTH - VALIDATION_RULE_DELTA + }) + }); + + expect(response.statusCode).toBe(HTTPCode.UNPROCESSED_ENTITY); + expect(response.json>().message).toBe( + UserValidationMessage.PASSWORD_MIN_LENGTH + ); + }); + + it(`should return ${HTTPCode.UNPROCESSED_ENTITY} of too long ${UserPayloadKey.PASSWORD} validation error`, async () => { + const [validTestUser] = TEST_USERS_CREDENTIALS; + + const response = await app + .inject() + .post(registerEndpoint) + .body({ + ...validTestUser, + [UserPayloadKey.PASSWORD]: faker.internet.password({ + length: + UserValidationRule.PASSWORD_MAX_LENGTH + VALIDATION_RULE_DELTA + }) + }); + + expect(response.statusCode).toBe(HTTPCode.UNPROCESSED_ENTITY); + expect(response.json>().message).toBe( + UserValidationMessage.PASSWORD_MAX_LENGTH + ); + }); + + it(`should return ${HTTPCode.CREATED} and create a new user`, async () => { + const [validTestUser] = TEST_USERS_CREDENTIALS as [UserSignUpRequestDto]; + + const response = await app + .inject() + .post(registerEndpoint) + .body(validTestUser); + + expect(response.statusCode).toBe(HTTPCode.CREATED); + expect(response.json()).toEqual( + expect.objectContaining({ + [UserPayloadKey.EMAIL]: validTestUser[UserPayloadKey.EMAIL] + }) + ); + + const savedDatabaseUser = await select({ + condition: { id: response.json().id }, + limit: KNEX_SELECT_ONE_RECORD, + table: DatabaseTableName.USERS + }); + + expect(savedDatabaseUser).toEqual( + expect.objectContaining({ + [UserPayloadKey.EMAIL]: validTestUser[UserPayloadKey.EMAIL] + }) + ); + }); + }); +}); diff --git a/server/tests/packages/user/libs/constants/constants.ts b/apps/backend/tests/modules/user/libs/constants/constants.ts similarity index 100% rename from server/tests/packages/user/libs/constants/constants.ts rename to apps/backend/tests/modules/user/libs/constants/constants.ts diff --git a/server/tests/packages/user/libs/constants/test-user-credentials.constant.ts b/apps/backend/tests/modules/user/libs/constants/test-user-credentials.constant.ts similarity index 57% rename from server/tests/packages/user/libs/constants/test-user-credentials.constant.ts rename to apps/backend/tests/modules/user/libs/constants/test-user-credentials.constant.ts index 80811fb5..66d191fa 100644 --- a/server/tests/packages/user/libs/constants/test-user-credentials.constant.ts +++ b/apps/backend/tests/modules/user/libs/constants/test-user-credentials.constant.ts @@ -1,15 +1,14 @@ import { faker } from '@faker-js/faker'; -import { type UserRegisterRequestDto } from 'shared/dist/packages/user/user.js'; -import { UserPayloadKey } from '~/packages/user/user.js'; +import { type UserSignUpRequestDto } from '~/modules/auth/auth.js'; +import { UserPayloadKey } from '~/modules/user/user.js'; const USERS_COUNT = 2; const TEST_USERS_CREDENTIALS = Array.from( { length: USERS_COUNT }, - (): UserRegisterRequestDto => { + (): UserSignUpRequestDto => { return { - [UserPayloadKey.USERNAME]: faker.person.firstName(), [UserPayloadKey.EMAIL]: faker.internet.email(), [UserPayloadKey.PASSWORD]: faker.internet.password() }; diff --git a/server/tests/packages/user/user.ts b/apps/backend/tests/modules/user/user.ts similarity index 56% rename from server/tests/packages/user/user.ts rename to apps/backend/tests/modules/user/user.ts index 9c1d7982..1e237bf1 100644 --- a/server/tests/packages/user/user.ts +++ b/apps/backend/tests/modules/user/user.ts @@ -1,2 +1 @@ -export { setupTestUsers } from './helpers/helpers.js'; export { TEST_USERS_CREDENTIALS } from './libs/constants/constants.js'; diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json new file mode 100644 index 00000000..2f1215fe --- /dev/null +++ b/apps/backend/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "include": ["**/*.ts", "**/*.js"], + "exclude": ["node_modules", "build"], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "outDir": "build" + } +} diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example new file mode 100644 index 00000000..a4037cc8 --- /dev/null +++ b/apps/frontend/.env.example @@ -0,0 +1,8 @@ +# +# APPLICATION +# +# default. own preference might be used +VITE_APP_PROXY_SERVER_URL=http://localhost:3001 +VITE_API_PATH=/api/v1 +VITE_APP_PORT=3000 +VITE_APP_HOST=localhost diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js new file mode 100644 index 00000000..abf5b044 --- /dev/null +++ b/apps/frontend/eslint.config.js @@ -0,0 +1,109 @@ +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import globals from 'globals'; + +import baseConfig from '../../eslint.config.js'; + +/** @typedef {import("eslint").Linter.FlatConfig} */ +let FlatConfig; + +/** @type {FlatConfig} */ +const ignoresConfig = { + ignores: ['build'] +}; + +/** @type {FlatConfig} */ +const mainConfig = { + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + JSX: true, + React: true + } + } +}; + +/** @type {FlatConfig} */ +const settingsConfig = { + settings: { + react: { version: 'detect' } + } +}; + +/** @type {FlatConfig} */ +const reactConfig = { + files: ['**/*.tsx'], + plugins: { + react + }, + rules: { + ...react.configs['jsx-runtime'].rules, + ...react.configs['recommended'].rules, + 'react/jsx-boolean-value': ['error'], + 'react/jsx-curly-brace-presence': ['error'], + 'react/jsx-no-bind': ['error', { ignoreRefs: true }], + 'react/prop-types': ['off'], + 'react/self-closing-comp': ['error'] + } +}; + +/** @type {FlatConfig} */ +const reactHooksConfig = { + files: ['**/*.tsx'], + plugins: { + 'react-hooks': reactHooks + }, + rules: reactHooks.configs.recommended.rules +}; + +/** @type {FlatConfig} */ +const jsxA11yConfig = { + files: ['**/*.tsx'], + plugins: { + 'jsx-a11y': jsxA11y + }, + rules: jsxA11y.configs.recommended.rules +}; + +/** @type {FlatConfig} */ +const explicitGenericsConfig = { + rules: { + 'require-explicit-generics/require-explicit-generics': [ + 'error', + ['useState'] + ] + } +}; + +/** @type {FlatConfig[]} */ +const overridesConfigs = [ + { + files: ['vite.config.ts'], + rules: { + 'import/no-default-export': ['off'] + } + }, + { + files: ['src/vite-env.d.ts'], + rules: { + 'unicorn/prevent-abbreviations': ['off'] + } + } +]; + +/** @type {FlatConfig[]} */ +const config = [ + ...baseConfig, + ignoresConfig, + mainConfig, + settingsConfig, + reactConfig, + reactHooksConfig, + jsxA11yConfig, + explicitGenericsConfig, + ...overridesConfigs +]; + +export default config; diff --git a/client/index.html b/apps/frontend/index.html similarity index 100% rename from client/index.html rename to apps/frontend/index.html diff --git a/apps/frontend/lint-staged.config.js b/apps/frontend/lint-staged.config.js new file mode 100644 index 00000000..7e4844af --- /dev/null +++ b/apps/frontend/lint-staged.config.js @@ -0,0 +1,13 @@ +import { default as baseConfig } from '../../lint-staged.config.js'; + +/** @type {import('lint-staged').Config} */ +const config = { + ...baseConfig, + '**/*.{ts,tsx}': [ + () => 'npm run lint:js -w apps/frontend', + () => 'npm run lint:type -w apps/frontend' + ], + '**/*.css': [() => 'npm run lint:css -w apps/frontend'] +}; + +export default config; diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 00000000..9f6c0624 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "@thread-js/frontend", + "private": true, + "engines": { + "node": "20.11.x", + "npm": "10.2.x" + }, + "type": "module", + "scripts": { + "start:dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint:css": "npx stylelint \"src/**/*.css\"", + "lint:js": "npx eslint . --max-warnings=0", + "lint:type": "npx tsc --noEmit", + "lint": "concurrently \"npm:lint:*\"" + }, + "dependencies": { + "@hookform/error-message": "2.0.1", + "@hookform/resolvers": "3.6.0", + "@reduxjs/toolkit": "2.2.5", + "@thread-js/shared": "*", + "clsx": "2.1.1", + "query-string": "9.0.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-hook-form": "7.51.5", + "react-redux": "9.1.2", + "react-router-dom": "6.23.1" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "devDependencies": { + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@vitejs/plugin-react": "4.3.1", + "eslint-plugin-jsx-a11y": "6.8.0", + "eslint-plugin-react": "7.34.2", + "eslint-plugin-react-hooks": "4.6.2", + "sass": "1.77.4", + "stylelint-config-standard-scss": "13.1.0", + "stylelint-scss": "6.3.1", + "vite": "5.2.13", + "vite-tsconfig-paths": "4.3.2" + } +} diff --git a/apps/frontend/packages.d.ts b/apps/frontend/packages.d.ts new file mode 100644 index 00000000..3bc11711 --- /dev/null +++ b/apps/frontend/packages.d.ts @@ -0,0 +1,32 @@ +declare module 'eslint-plugin-react' { + import { type Linter } from 'eslint'; + + const configs: Record< + 'jsx-runtime' | 'recommended', + Required + >; + + export default { + configs + }; +} + +declare module 'eslint-plugin-react-hooks' { + import { type Linter } from 'eslint'; + + const configs: Record<'recommended', Required>; + + export default { + configs + }; +} + +declare module 'eslint-plugin-jsx-a11y' { + import { type Linter } from 'eslint'; + + const configs: Record<'recommended', Required>; + + export default { + configs + }; +} diff --git a/client/public/favicon.ico b/apps/frontend/public/favicon.ico similarity index 100% rename from client/public/favicon.ico rename to apps/frontend/public/favicon.ico diff --git a/client/public/manifest.json b/apps/frontend/public/manifest.json similarity index 100% rename from client/public/manifest.json rename to apps/frontend/public/manifest.json diff --git a/client/src/assets/css/common.scss b/apps/frontend/src/assets/css/scaffolding.css similarity index 93% rename from client/src/assets/css/common.scss rename to apps/frontend/src/assets/css/scaffolding.css index f4a2edc5..90967e97 100644 --- a/client/src/assets/css/common.scss +++ b/apps/frontend/src/assets/css/scaffolding.css @@ -3,16 +3,16 @@ } body { - height: 100%; min-width: 320px; - margin: 0; + height: 100%; padding: 0; - background-color: #f6f6f6; + margin: 0; overflow-x: hidden; font-family: Lato, Arial, sans-serif; font-size: 14px; - line-height: 1.4285em; + line-height: 1.5; color: rgba(0 0 0 / 87%); + background-color: #f6f6f6; } html, diff --git a/apps/frontend/src/assets/css/styles.css b/apps/frontend/src/assets/css/styles.css new file mode 100644 index 00000000..01f4eca3 --- /dev/null +++ b/apps/frontend/src/assets/css/styles.css @@ -0,0 +1,2 @@ +@import './scaffolding.css'; +@import './variables.css'; diff --git a/client/src/assets/css/variables.scss b/apps/frontend/src/assets/css/variables.css similarity index 81% rename from client/src/assets/css/variables.scss rename to apps/frontend/src/assets/css/variables.css index 0b248881..23c75538 100644 --- a/client/src/assets/css/variables.scss +++ b/apps/frontend/src/assets/css/variables.css @@ -1,6 +1,6 @@ :root { --blue: #2185d0; --teal: #00b5ad; - --white: #fff; + --white: #ffffff; --light-shadow: rgba(34 36 38 / 15%); } diff --git a/client/src/index.tsx b/apps/frontend/src/index.tsx similarity index 55% rename from client/src/index.tsx rename to apps/frontend/src/index.tsx index d6bd289b..b312b8f8 100644 --- a/client/src/index.tsx +++ b/apps/frontend/src/index.tsx @@ -1,21 +1,18 @@ -import '~/assets/css/styles.scss'; - import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { store } from '~/libs/packages/store/store.js'; -import { App } from '~/pages/app/app.js'; +import '~/assets/css/styles.css'; +import { store } from '~/libs/modules/store/store.js'; + +import { App } from './pages/app/app.js'; const root = createRoot(document.querySelector('#root') as HTMLElement); root.render( - - - + ); diff --git a/client/src/libs/components/button/button.tsx b/apps/frontend/src/libs/components/button/button.tsx similarity index 68% rename from client/src/libs/components/button/button.tsx rename to apps/frontend/src/libs/components/button/button.tsx index 24d2bc29..2ee5f7f0 100644 --- a/client/src/libs/components/button/button.tsx +++ b/apps/frontend/src/libs/components/button/button.tsx @@ -1,53 +1,39 @@ /* eslint-disable react/button-has-type */ -import { type SizeProp } from '@fortawesome/fontawesome-svg-core'; import clsx from 'clsx'; import { type ReactNode } from 'react'; -import { - type ButtonColor, - type IconName, - type IconSize -} from '~/libs/enums/enums.js'; +import { type ButtonColor } from '~/libs/enums/enums.js'; import { type ButtonType, type ValueOf } from '~/libs/types/types.js'; -import { Icon } from '../icon/icon.js'; import styles from './styles.module.scss'; type ButtonProperties = { children?: ReactNode; - type?: ButtonType; - color?: ValueOf; - onClick?: React.MouseEventHandler; className?: string; - iconName?: ValueOf; - iconSize?: ValueOf; + color?: ValueOf; isBasic?: boolean; + isDisabled?: boolean; isFluid?: boolean; isLoading?: boolean; isPrimary?: boolean; - isDisabled?: boolean; + onClick?: React.MouseEventHandler; + type?: ButtonType; }; const Button: React.FC = ({ - onClick, + children, className, - type = 'button', color, - iconName, - iconSize, isBasic = false, + isDisabled = false, isFluid = false, isLoading = false, isPrimary = false, - isDisabled = false, - children + onClick, + type = 'button' }) => { - const hasIcon = Boolean(iconName); - return ( ); diff --git a/client/src/libs/components/button/styles.module.scss b/apps/frontend/src/libs/components/button/styles.module.scss similarity index 100% rename from client/src/libs/components/button/styles.module.scss rename to apps/frontend/src/libs/components/button/styles.module.scss diff --git a/apps/frontend/src/libs/components/components.ts b/apps/frontend/src/libs/components/components.ts new file mode 100644 index 00000000..86a7f82e --- /dev/null +++ b/apps/frontend/src/libs/components/components.ts @@ -0,0 +1,5 @@ +export { Button } from './button/button.js'; +export { Image } from './image/image.js'; +export { Input } from './input/input.js'; +export { RouterProvider } from './router-provider/router-provider.js'; +export { NavLink } from 'react-router-dom'; diff --git a/client/src/libs/components/image/image.tsx b/apps/frontend/src/libs/components/image/image.tsx similarity index 100% rename from client/src/libs/components/image/image.tsx rename to apps/frontend/src/libs/components/image/image.tsx index 5a390b7f..b29a5a40 100644 --- a/client/src/libs/components/image/image.tsx +++ b/apps/frontend/src/libs/components/image/image.tsx @@ -7,26 +7,27 @@ import styles from './styles.module.scss'; type ImageProperties = { alt: string; - isCentered?: boolean; - isCircular?: boolean; className?: string; height?: string; + isCentered?: boolean; + isCircular?: boolean; size?: ValueOf; - width?: string; src: string; + width?: string; }; const Image: React.FC = ({ alt, - isCentered, - isCircular, className, height, + isCentered, + isCircular, size, src, width }) => ( {alt} = ({ size && styles[`imageSize__${size}`], className )} - width={width} height={height} src={src} - alt={alt} + width={width} /> ); diff --git a/client/src/libs/components/image/styles.module.scss b/apps/frontend/src/libs/components/image/styles.module.scss similarity index 100% rename from client/src/libs/components/image/styles.module.scss rename to apps/frontend/src/libs/components/image/styles.module.scss diff --git a/client/src/libs/components/input/input.tsx b/apps/frontend/src/libs/components/input/input.tsx similarity index 71% rename from client/src/libs/components/input/input.tsx rename to apps/frontend/src/libs/components/input/input.tsx index 28b6c5ba..bbd669e3 100644 --- a/client/src/libs/components/input/input.tsx +++ b/apps/frontend/src/libs/components/input/input.tsx @@ -7,66 +7,52 @@ import { type FieldValues } from 'react-hook-form'; -import { type IconName } from '~/libs/enums/enums.js'; import { useController } from '~/libs/hooks/hooks.js'; -import { type ValueOf } from '~/libs/types/types.js'; -import { Icon } from '../icon/icon.js'; import styles from './styles.module.scss'; type InputProperties = { - name: FieldPath; + className?: string; control: Control; - errors?: object; disabled?: boolean; - iconName?: ValueOf; + errors?: object; + name: FieldPath; placeholder: string; - className?: string; - type?: 'email' | 'password' | 'submit' | 'text'; rows?: number; + type?: 'email' | 'password' | 'submit' | 'text'; }; const Input = ({ - name, + className, control, - type = 'text', - rows = 0, - errors = {}, disabled, - iconName, + errors = {}, + name, placeholder, - className + rows, + type = 'text' }: InputProperties): ReactElement => { - const { field } = useController({ name, control }); + const { field } = useController({ control, name }); const isTextarea = Boolean(rows); return (
- {iconName && ( - - - - )} {isTextarea ? (