From 0f6bc31ec1917a342327319ce797147f7daba65d Mon Sep 17 00:00:00 2001 From: Husen Date: Sun, 10 Dec 2023 22:04:42 +0700 Subject: [PATCH 1/8] chore: change folder structrure Signed-off-by: Husen --- .github/workflows/node.yml | 8 +- {node => backend/node}/.editorconfig | 0 {node => backend/node}/.env.example | 0 {node => backend/node}/.eslintrc.js | 0 {node => backend/node}/.gitignore | 0 {node => backend/node}/.prettierignore | 0 {node => backend/node}/.prettierrc.js | 0 {node => backend/node}/__tests__/app.test.ts | 0 .../node}/__tests__/fixtures/user.fixture.ts | 0 {node => backend/node}/__tests__/setup.ts | 0 {node => backend/node}/__tests__/teardown.ts | 0 {node => backend/node}/__tests__/user.test.ts | 0 .../20220530152859_create_users_tables.js | 0 .../node}/database/seeds/users.js | 0 {node => backend/node}/jest.config.ts | 0 {node => backend/node}/knexfile.js | 0 {node => backend/node}/package.json | 0 {node => backend/node}/pnpm-lock.yaml | 0 {node => backend/node}/pnpm-workspace.yaml | 0 .../create-user.controller.spec.ts | 0 .../controllers/create-user.controller.ts | 0 .../find-all-users.controller.spec.ts | 0 .../controllers/find-all-users.controller.ts | 0 .../find-one-user.controller.spec.ts | 0 .../controllers/find-one-user.controller.ts | 0 .../controllers/home.controller.spec.ts | 0 .../adapters/controllers/home.controller.ts | 0 .../remove-user.controller.spec.ts | 0 .../controllers/remove-user.controller.ts | 0 .../unique-user-email.controller.spec.ts | 0 .../unique-user-email.controller.ts | 0 .../update-user.controller.spec.ts | 0 .../controllers/update-user.controller.ts | 0 .../validate-uuid.controller.spec.ts | 0 .../controllers/validate-uuid.controller.ts | 0 .../adapters/interfaces/common.interface.ts | 0 .../src/adapters/interfaces/http.interface.ts | 0 .../src/adapters/interfaces/user.interface.ts | 0 .../node}/src/core/entities/common.entity.ts | 0 .../node}/src/core/entities/user.entity.ts | 0 .../core/exceptions/bad-request.exception.ts | 0 .../src/core/exceptions/http.exception.ts | 0 .../core/exceptions/not-found.exception.ts | 0 .../src/core/interfaces/common.interface.ts | 0 .../src/core/interfaces/file.interface.ts | 0 .../src/core/interfaces/hash.interface.ts | 0 .../src/core/interfaces/http.interface.ts | 0 .../src/core/interfaces/redis.interface.ts | 0 .../src/core/interfaces/user.interface.ts | 0 .../use-cases/create-user.use-case.spec.ts | 0 .../core/use-cases/create-user.use-case.ts | 0 .../use-cases/find-all-users.use-case.spec.ts | 0 .../core/use-cases/find-all-users.use-case.ts | 0 .../use-cases/find-one-user.use-case.spec.ts | 0 .../core/use-cases/find-one-user.use-case.ts | 0 .../use-cases/remove-user.use-case.spec.ts | 0 .../core/use-cases/remove-user.use-case.ts | 0 .../unique-user-email.use-case.spec.ts | 0 .../use-cases/unique-user-email.use-case.ts | 0 .../use-cases/update-user.use-case.spec.ts | 0 .../core/use-cases/update-user.use-case.ts | 0 .../use-cases/validate-uuid.use-case.spec.ts | 0 .../core/use-cases/validate-uuid.use-case.ts | 0 .../node}/src/infrastructure/config/app.ts | 0 .../node}/src/infrastructure/config/auth.ts | 0 .../src/infrastructure/config/database.ts | 0 .../node}/src/infrastructure/config/redis.ts | 0 .../node}/src/infrastructure/config/s3.ts | 0 .../src/infrastructure/ports/database.ts | 0 .../node}/src/infrastructure/ports/logger.ts | 0 .../node}/src/infrastructure/ports/redis.ts | 0 .../node}/src/infrastructure/ports/s3.ts | 0 .../repositories/user.repository.ts | 0 .../src/infrastructure/server/express/app.ts | 0 .../server/express/handlers/app.handler.ts | 0 .../server/express/handlers/user.handler.ts | 0 .../infrastructure/server/express/index.ts | 0 .../server/express/middlewares/error.ts | 0 .../server/express/middlewares/logger.ts | 0 .../express/middlewares/unique-user-email.ts | 0 .../express/middlewares/validate-uuid.ts | 0 .../server/express/middlewares/validator.ts | 0 .../server/express/package.json | 0 .../server/express/pnpm-lock.yaml | 0 .../server/express/routes/app.route.ts | 2 +- .../server/express/routes/index.ts | 0 .../server/express/routes/user.route.ts | 0 .../server/express/schemas/auth.schema.ts | 0 .../server/express/schemas/common.schema.ts | 0 .../server/express/schemas/user.schema.ts | 0 .../index.d.ts | 0 .../server/express/types/express/index.d.ts | 0 .../infrastructure/services/file.service.ts | 0 .../infrastructure/services/hash.service.ts | 0 .../infrastructure/services/redis.service.ts | 0 {node => backend/node}/tsconfig.base.json | 0 {node => backend/node}/tsconfig.eslint.json | 0 {node => backend/node}/tsconfig.json | 0 backend/node/tsconfig.tsnode.json | 106 +++++++ {node => backend/node}/vitest.config.ts | 0 .../email-password/identity.schema.json | 49 ++++ config/kratos/email-password/kratos.yml | 102 +++++++ config/mailhog/auth | 1 + config/oathkeeper/access-rules.yml | 60 ++++ config/oathkeeper/id_token.jwks.json | 18 ++ config/oathkeeper/oathkeeper.yml | 88 ++++++ docker-compose-express.yml | 0 docker-compose-gateway.yml | 84 ++++++ docker-compose-sveltekit.yml | 0 node/docker-compose.yml => docker-compose.yml | 35 ++- node/tsconfig.tsnode.json | 100 ------- openapi.yaml | 264 +----------------- 112 files changed, 550 insertions(+), 367 deletions(-) rename {node => backend/node}/.editorconfig (100%) rename {node => backend/node}/.env.example (100%) rename {node => backend/node}/.eslintrc.js (100%) rename {node => backend/node}/.gitignore (100%) rename {node => backend/node}/.prettierignore (100%) rename {node => backend/node}/.prettierrc.js (100%) rename {node => backend/node}/__tests__/app.test.ts (100%) rename {node => backend/node}/__tests__/fixtures/user.fixture.ts (100%) rename {node => backend/node}/__tests__/setup.ts (100%) rename {node => backend/node}/__tests__/teardown.ts (100%) rename {node => backend/node}/__tests__/user.test.ts (100%) rename {node => backend/node}/database/migrations/20220530152859_create_users_tables.js (100%) rename {node => backend/node}/database/seeds/users.js (100%) rename {node => backend/node}/jest.config.ts (100%) rename {node => backend/node}/knexfile.js (100%) rename {node => backend/node}/package.json (100%) rename {node => backend/node}/pnpm-lock.yaml (100%) rename {node => backend/node}/pnpm-workspace.yaml (100%) rename {node => backend/node}/src/adapters/controllers/create-user.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/create-user.controller.ts (100%) rename {node => backend/node}/src/adapters/controllers/find-all-users.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/find-all-users.controller.ts (100%) rename {node => backend/node}/src/adapters/controllers/find-one-user.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/find-one-user.controller.ts (100%) rename {node => backend/node}/src/adapters/controllers/home.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/home.controller.ts (100%) rename {node => backend/node}/src/adapters/controllers/remove-user.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/remove-user.controller.ts (100%) rename {node => backend/node}/src/adapters/controllers/unique-user-email.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/unique-user-email.controller.ts (100%) rename {node => backend/node}/src/adapters/controllers/update-user.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/update-user.controller.ts (100%) rename {node => backend/node}/src/adapters/controllers/validate-uuid.controller.spec.ts (100%) rename {node => backend/node}/src/adapters/controllers/validate-uuid.controller.ts (100%) rename {node => backend/node}/src/adapters/interfaces/common.interface.ts (100%) rename {node => backend/node}/src/adapters/interfaces/http.interface.ts (100%) rename {node => backend/node}/src/adapters/interfaces/user.interface.ts (100%) rename {node => backend/node}/src/core/entities/common.entity.ts (100%) rename {node => backend/node}/src/core/entities/user.entity.ts (100%) rename {node => backend/node}/src/core/exceptions/bad-request.exception.ts (100%) rename {node => backend/node}/src/core/exceptions/http.exception.ts (100%) rename {node => backend/node}/src/core/exceptions/not-found.exception.ts (100%) rename {node => backend/node}/src/core/interfaces/common.interface.ts (100%) rename {node => backend/node}/src/core/interfaces/file.interface.ts (100%) rename {node => backend/node}/src/core/interfaces/hash.interface.ts (100%) rename {node => backend/node}/src/core/interfaces/http.interface.ts (100%) rename {node => backend/node}/src/core/interfaces/redis.interface.ts (100%) rename {node => backend/node}/src/core/interfaces/user.interface.ts (100%) rename {node => backend/node}/src/core/use-cases/create-user.use-case.spec.ts (100%) rename {node => backend/node}/src/core/use-cases/create-user.use-case.ts (100%) rename {node => backend/node}/src/core/use-cases/find-all-users.use-case.spec.ts (100%) rename {node => backend/node}/src/core/use-cases/find-all-users.use-case.ts (100%) rename {node => backend/node}/src/core/use-cases/find-one-user.use-case.spec.ts (100%) rename {node => backend/node}/src/core/use-cases/find-one-user.use-case.ts (100%) rename {node => backend/node}/src/core/use-cases/remove-user.use-case.spec.ts (100%) rename {node => backend/node}/src/core/use-cases/remove-user.use-case.ts (100%) rename {node => backend/node}/src/core/use-cases/unique-user-email.use-case.spec.ts (100%) rename {node => backend/node}/src/core/use-cases/unique-user-email.use-case.ts (100%) rename {node => backend/node}/src/core/use-cases/update-user.use-case.spec.ts (100%) rename {node => backend/node}/src/core/use-cases/update-user.use-case.ts (100%) rename {node => backend/node}/src/core/use-cases/validate-uuid.use-case.spec.ts (100%) rename {node => backend/node}/src/core/use-cases/validate-uuid.use-case.ts (100%) rename {node => backend/node}/src/infrastructure/config/app.ts (100%) rename {node => backend/node}/src/infrastructure/config/auth.ts (100%) rename {node => backend/node}/src/infrastructure/config/database.ts (100%) rename {node => backend/node}/src/infrastructure/config/redis.ts (100%) rename {node => backend/node}/src/infrastructure/config/s3.ts (100%) rename {node => backend/node}/src/infrastructure/ports/database.ts (100%) rename {node => backend/node}/src/infrastructure/ports/logger.ts (100%) rename {node => backend/node}/src/infrastructure/ports/redis.ts (100%) rename {node => backend/node}/src/infrastructure/ports/s3.ts (100%) rename {node => backend/node}/src/infrastructure/repositories/user.repository.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/app.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/handlers/app.handler.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/handlers/user.handler.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/index.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/middlewares/error.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/middlewares/logger.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/middlewares/unique-user-email.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/middlewares/validate-uuid.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/middlewares/validator.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/package.json (100%) rename {node => backend/node}/src/infrastructure/server/express/pnpm-lock.yaml (100%) rename {node => backend/node}/src/infrastructure/server/express/routes/app.route.ts (87%) rename {node => backend/node}/src/infrastructure/server/express/routes/index.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/routes/user.route.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/schemas/auth.schema.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/schemas/common.schema.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/schemas/user.schema.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts (100%) rename {node => backend/node}/src/infrastructure/server/express/types/express/index.d.ts (100%) rename {node => backend/node}/src/infrastructure/services/file.service.ts (100%) rename {node => backend/node}/src/infrastructure/services/hash.service.ts (100%) rename {node => backend/node}/src/infrastructure/services/redis.service.ts (100%) rename {node => backend/node}/tsconfig.base.json (100%) rename {node => backend/node}/tsconfig.eslint.json (100%) rename {node => backend/node}/tsconfig.json (100%) create mode 100644 backend/node/tsconfig.tsnode.json rename {node => backend/node}/vitest.config.ts (100%) create mode 100644 config/kratos/email-password/identity.schema.json create mode 100644 config/kratos/email-password/kratos.yml create mode 100644 config/mailhog/auth create mode 100644 config/oathkeeper/access-rules.yml create mode 100644 config/oathkeeper/id_token.jwks.json create mode 100644 config/oathkeeper/oathkeeper.yml create mode 100644 docker-compose-express.yml create mode 100644 docker-compose-gateway.yml create mode 100644 docker-compose-sveltekit.yml rename node/docker-compose.yml => docker-compose.yml (74%) delete mode 100644 node/tsconfig.tsnode.json diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index fb6d2f4..dc57983 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -6,14 +6,14 @@ on: - "main" - "development" paths: - - "node/**" + - "backend/node/**" - ".github/workflows/node.yml" pull_request: branches: - "main" - "development" paths: - - "node/**" + - "backend/node/**" - ".github/workflows/node.yml" jobs: @@ -22,9 +22,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ['18', '20'] + node: ['18', '20', '21'] env: - FOLDER: node + FOLDER: backend/node steps: - name: Checkout code diff --git a/node/.editorconfig b/backend/node/.editorconfig similarity index 100% rename from node/.editorconfig rename to backend/node/.editorconfig diff --git a/node/.env.example b/backend/node/.env.example similarity index 100% rename from node/.env.example rename to backend/node/.env.example diff --git a/node/.eslintrc.js b/backend/node/.eslintrc.js similarity index 100% rename from node/.eslintrc.js rename to backend/node/.eslintrc.js diff --git a/node/.gitignore b/backend/node/.gitignore similarity index 100% rename from node/.gitignore rename to backend/node/.gitignore diff --git a/node/.prettierignore b/backend/node/.prettierignore similarity index 100% rename from node/.prettierignore rename to backend/node/.prettierignore diff --git a/node/.prettierrc.js b/backend/node/.prettierrc.js similarity index 100% rename from node/.prettierrc.js rename to backend/node/.prettierrc.js diff --git a/node/__tests__/app.test.ts b/backend/node/__tests__/app.test.ts similarity index 100% rename from node/__tests__/app.test.ts rename to backend/node/__tests__/app.test.ts diff --git a/node/__tests__/fixtures/user.fixture.ts b/backend/node/__tests__/fixtures/user.fixture.ts similarity index 100% rename from node/__tests__/fixtures/user.fixture.ts rename to backend/node/__tests__/fixtures/user.fixture.ts diff --git a/node/__tests__/setup.ts b/backend/node/__tests__/setup.ts similarity index 100% rename from node/__tests__/setup.ts rename to backend/node/__tests__/setup.ts diff --git a/node/__tests__/teardown.ts b/backend/node/__tests__/teardown.ts similarity index 100% rename from node/__tests__/teardown.ts rename to backend/node/__tests__/teardown.ts diff --git a/node/__tests__/user.test.ts b/backend/node/__tests__/user.test.ts similarity index 100% rename from node/__tests__/user.test.ts rename to backend/node/__tests__/user.test.ts diff --git a/node/database/migrations/20220530152859_create_users_tables.js b/backend/node/database/migrations/20220530152859_create_users_tables.js similarity index 100% rename from node/database/migrations/20220530152859_create_users_tables.js rename to backend/node/database/migrations/20220530152859_create_users_tables.js diff --git a/node/database/seeds/users.js b/backend/node/database/seeds/users.js similarity index 100% rename from node/database/seeds/users.js rename to backend/node/database/seeds/users.js diff --git a/node/jest.config.ts b/backend/node/jest.config.ts similarity index 100% rename from node/jest.config.ts rename to backend/node/jest.config.ts diff --git a/node/knexfile.js b/backend/node/knexfile.js similarity index 100% rename from node/knexfile.js rename to backend/node/knexfile.js diff --git a/node/package.json b/backend/node/package.json similarity index 100% rename from node/package.json rename to backend/node/package.json diff --git a/node/pnpm-lock.yaml b/backend/node/pnpm-lock.yaml similarity index 100% rename from node/pnpm-lock.yaml rename to backend/node/pnpm-lock.yaml diff --git a/node/pnpm-workspace.yaml b/backend/node/pnpm-workspace.yaml similarity index 100% rename from node/pnpm-workspace.yaml rename to backend/node/pnpm-workspace.yaml diff --git a/node/src/adapters/controllers/create-user.controller.spec.ts b/backend/node/src/adapters/controllers/create-user.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/create-user.controller.spec.ts rename to backend/node/src/adapters/controllers/create-user.controller.spec.ts diff --git a/node/src/adapters/controllers/create-user.controller.ts b/backend/node/src/adapters/controllers/create-user.controller.ts similarity index 100% rename from node/src/adapters/controllers/create-user.controller.ts rename to backend/node/src/adapters/controllers/create-user.controller.ts diff --git a/node/src/adapters/controllers/find-all-users.controller.spec.ts b/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/find-all-users.controller.spec.ts rename to backend/node/src/adapters/controllers/find-all-users.controller.spec.ts diff --git a/node/src/adapters/controllers/find-all-users.controller.ts b/backend/node/src/adapters/controllers/find-all-users.controller.ts similarity index 100% rename from node/src/adapters/controllers/find-all-users.controller.ts rename to backend/node/src/adapters/controllers/find-all-users.controller.ts diff --git a/node/src/adapters/controllers/find-one-user.controller.spec.ts b/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/find-one-user.controller.spec.ts rename to backend/node/src/adapters/controllers/find-one-user.controller.spec.ts diff --git a/node/src/adapters/controllers/find-one-user.controller.ts b/backend/node/src/adapters/controllers/find-one-user.controller.ts similarity index 100% rename from node/src/adapters/controllers/find-one-user.controller.ts rename to backend/node/src/adapters/controllers/find-one-user.controller.ts diff --git a/node/src/adapters/controllers/home.controller.spec.ts b/backend/node/src/adapters/controllers/home.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/home.controller.spec.ts rename to backend/node/src/adapters/controllers/home.controller.spec.ts diff --git a/node/src/adapters/controllers/home.controller.ts b/backend/node/src/adapters/controllers/home.controller.ts similarity index 100% rename from node/src/adapters/controllers/home.controller.ts rename to backend/node/src/adapters/controllers/home.controller.ts diff --git a/node/src/adapters/controllers/remove-user.controller.spec.ts b/backend/node/src/adapters/controllers/remove-user.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/remove-user.controller.spec.ts rename to backend/node/src/adapters/controllers/remove-user.controller.spec.ts diff --git a/node/src/adapters/controllers/remove-user.controller.ts b/backend/node/src/adapters/controllers/remove-user.controller.ts similarity index 100% rename from node/src/adapters/controllers/remove-user.controller.ts rename to backend/node/src/adapters/controllers/remove-user.controller.ts diff --git a/node/src/adapters/controllers/unique-user-email.controller.spec.ts b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/unique-user-email.controller.spec.ts rename to backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts diff --git a/node/src/adapters/controllers/unique-user-email.controller.ts b/backend/node/src/adapters/controllers/unique-user-email.controller.ts similarity index 100% rename from node/src/adapters/controllers/unique-user-email.controller.ts rename to backend/node/src/adapters/controllers/unique-user-email.controller.ts diff --git a/node/src/adapters/controllers/update-user.controller.spec.ts b/backend/node/src/adapters/controllers/update-user.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/update-user.controller.spec.ts rename to backend/node/src/adapters/controllers/update-user.controller.spec.ts diff --git a/node/src/adapters/controllers/update-user.controller.ts b/backend/node/src/adapters/controllers/update-user.controller.ts similarity index 100% rename from node/src/adapters/controllers/update-user.controller.ts rename to backend/node/src/adapters/controllers/update-user.controller.ts diff --git a/node/src/adapters/controllers/validate-uuid.controller.spec.ts b/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts similarity index 100% rename from node/src/adapters/controllers/validate-uuid.controller.spec.ts rename to backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts diff --git a/node/src/adapters/controllers/validate-uuid.controller.ts b/backend/node/src/adapters/controllers/validate-uuid.controller.ts similarity index 100% rename from node/src/adapters/controllers/validate-uuid.controller.ts rename to backend/node/src/adapters/controllers/validate-uuid.controller.ts diff --git a/node/src/adapters/interfaces/common.interface.ts b/backend/node/src/adapters/interfaces/common.interface.ts similarity index 100% rename from node/src/adapters/interfaces/common.interface.ts rename to backend/node/src/adapters/interfaces/common.interface.ts diff --git a/node/src/adapters/interfaces/http.interface.ts b/backend/node/src/adapters/interfaces/http.interface.ts similarity index 100% rename from node/src/adapters/interfaces/http.interface.ts rename to backend/node/src/adapters/interfaces/http.interface.ts diff --git a/node/src/adapters/interfaces/user.interface.ts b/backend/node/src/adapters/interfaces/user.interface.ts similarity index 100% rename from node/src/adapters/interfaces/user.interface.ts rename to backend/node/src/adapters/interfaces/user.interface.ts diff --git a/node/src/core/entities/common.entity.ts b/backend/node/src/core/entities/common.entity.ts similarity index 100% rename from node/src/core/entities/common.entity.ts rename to backend/node/src/core/entities/common.entity.ts diff --git a/node/src/core/entities/user.entity.ts b/backend/node/src/core/entities/user.entity.ts similarity index 100% rename from node/src/core/entities/user.entity.ts rename to backend/node/src/core/entities/user.entity.ts diff --git a/node/src/core/exceptions/bad-request.exception.ts b/backend/node/src/core/exceptions/bad-request.exception.ts similarity index 100% rename from node/src/core/exceptions/bad-request.exception.ts rename to backend/node/src/core/exceptions/bad-request.exception.ts diff --git a/node/src/core/exceptions/http.exception.ts b/backend/node/src/core/exceptions/http.exception.ts similarity index 100% rename from node/src/core/exceptions/http.exception.ts rename to backend/node/src/core/exceptions/http.exception.ts diff --git a/node/src/core/exceptions/not-found.exception.ts b/backend/node/src/core/exceptions/not-found.exception.ts similarity index 100% rename from node/src/core/exceptions/not-found.exception.ts rename to backend/node/src/core/exceptions/not-found.exception.ts diff --git a/node/src/core/interfaces/common.interface.ts b/backend/node/src/core/interfaces/common.interface.ts similarity index 100% rename from node/src/core/interfaces/common.interface.ts rename to backend/node/src/core/interfaces/common.interface.ts diff --git a/node/src/core/interfaces/file.interface.ts b/backend/node/src/core/interfaces/file.interface.ts similarity index 100% rename from node/src/core/interfaces/file.interface.ts rename to backend/node/src/core/interfaces/file.interface.ts diff --git a/node/src/core/interfaces/hash.interface.ts b/backend/node/src/core/interfaces/hash.interface.ts similarity index 100% rename from node/src/core/interfaces/hash.interface.ts rename to backend/node/src/core/interfaces/hash.interface.ts diff --git a/node/src/core/interfaces/http.interface.ts b/backend/node/src/core/interfaces/http.interface.ts similarity index 100% rename from node/src/core/interfaces/http.interface.ts rename to backend/node/src/core/interfaces/http.interface.ts diff --git a/node/src/core/interfaces/redis.interface.ts b/backend/node/src/core/interfaces/redis.interface.ts similarity index 100% rename from node/src/core/interfaces/redis.interface.ts rename to backend/node/src/core/interfaces/redis.interface.ts diff --git a/node/src/core/interfaces/user.interface.ts b/backend/node/src/core/interfaces/user.interface.ts similarity index 100% rename from node/src/core/interfaces/user.interface.ts rename to backend/node/src/core/interfaces/user.interface.ts diff --git a/node/src/core/use-cases/create-user.use-case.spec.ts b/backend/node/src/core/use-cases/create-user.use-case.spec.ts similarity index 100% rename from node/src/core/use-cases/create-user.use-case.spec.ts rename to backend/node/src/core/use-cases/create-user.use-case.spec.ts diff --git a/node/src/core/use-cases/create-user.use-case.ts b/backend/node/src/core/use-cases/create-user.use-case.ts similarity index 100% rename from node/src/core/use-cases/create-user.use-case.ts rename to backend/node/src/core/use-cases/create-user.use-case.ts diff --git a/node/src/core/use-cases/find-all-users.use-case.spec.ts b/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts similarity index 100% rename from node/src/core/use-cases/find-all-users.use-case.spec.ts rename to backend/node/src/core/use-cases/find-all-users.use-case.spec.ts diff --git a/node/src/core/use-cases/find-all-users.use-case.ts b/backend/node/src/core/use-cases/find-all-users.use-case.ts similarity index 100% rename from node/src/core/use-cases/find-all-users.use-case.ts rename to backend/node/src/core/use-cases/find-all-users.use-case.ts diff --git a/node/src/core/use-cases/find-one-user.use-case.spec.ts b/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts similarity index 100% rename from node/src/core/use-cases/find-one-user.use-case.spec.ts rename to backend/node/src/core/use-cases/find-one-user.use-case.spec.ts diff --git a/node/src/core/use-cases/find-one-user.use-case.ts b/backend/node/src/core/use-cases/find-one-user.use-case.ts similarity index 100% rename from node/src/core/use-cases/find-one-user.use-case.ts rename to backend/node/src/core/use-cases/find-one-user.use-case.ts diff --git a/node/src/core/use-cases/remove-user.use-case.spec.ts b/backend/node/src/core/use-cases/remove-user.use-case.spec.ts similarity index 100% rename from node/src/core/use-cases/remove-user.use-case.spec.ts rename to backend/node/src/core/use-cases/remove-user.use-case.spec.ts diff --git a/node/src/core/use-cases/remove-user.use-case.ts b/backend/node/src/core/use-cases/remove-user.use-case.ts similarity index 100% rename from node/src/core/use-cases/remove-user.use-case.ts rename to backend/node/src/core/use-cases/remove-user.use-case.ts diff --git a/node/src/core/use-cases/unique-user-email.use-case.spec.ts b/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts similarity index 100% rename from node/src/core/use-cases/unique-user-email.use-case.spec.ts rename to backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts diff --git a/node/src/core/use-cases/unique-user-email.use-case.ts b/backend/node/src/core/use-cases/unique-user-email.use-case.ts similarity index 100% rename from node/src/core/use-cases/unique-user-email.use-case.ts rename to backend/node/src/core/use-cases/unique-user-email.use-case.ts diff --git a/node/src/core/use-cases/update-user.use-case.spec.ts b/backend/node/src/core/use-cases/update-user.use-case.spec.ts similarity index 100% rename from node/src/core/use-cases/update-user.use-case.spec.ts rename to backend/node/src/core/use-cases/update-user.use-case.spec.ts diff --git a/node/src/core/use-cases/update-user.use-case.ts b/backend/node/src/core/use-cases/update-user.use-case.ts similarity index 100% rename from node/src/core/use-cases/update-user.use-case.ts rename to backend/node/src/core/use-cases/update-user.use-case.ts diff --git a/node/src/core/use-cases/validate-uuid.use-case.spec.ts b/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts similarity index 100% rename from node/src/core/use-cases/validate-uuid.use-case.spec.ts rename to backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts diff --git a/node/src/core/use-cases/validate-uuid.use-case.ts b/backend/node/src/core/use-cases/validate-uuid.use-case.ts similarity index 100% rename from node/src/core/use-cases/validate-uuid.use-case.ts rename to backend/node/src/core/use-cases/validate-uuid.use-case.ts diff --git a/node/src/infrastructure/config/app.ts b/backend/node/src/infrastructure/config/app.ts similarity index 100% rename from node/src/infrastructure/config/app.ts rename to backend/node/src/infrastructure/config/app.ts diff --git a/node/src/infrastructure/config/auth.ts b/backend/node/src/infrastructure/config/auth.ts similarity index 100% rename from node/src/infrastructure/config/auth.ts rename to backend/node/src/infrastructure/config/auth.ts diff --git a/node/src/infrastructure/config/database.ts b/backend/node/src/infrastructure/config/database.ts similarity index 100% rename from node/src/infrastructure/config/database.ts rename to backend/node/src/infrastructure/config/database.ts diff --git a/node/src/infrastructure/config/redis.ts b/backend/node/src/infrastructure/config/redis.ts similarity index 100% rename from node/src/infrastructure/config/redis.ts rename to backend/node/src/infrastructure/config/redis.ts diff --git a/node/src/infrastructure/config/s3.ts b/backend/node/src/infrastructure/config/s3.ts similarity index 100% rename from node/src/infrastructure/config/s3.ts rename to backend/node/src/infrastructure/config/s3.ts diff --git a/node/src/infrastructure/ports/database.ts b/backend/node/src/infrastructure/ports/database.ts similarity index 100% rename from node/src/infrastructure/ports/database.ts rename to backend/node/src/infrastructure/ports/database.ts diff --git a/node/src/infrastructure/ports/logger.ts b/backend/node/src/infrastructure/ports/logger.ts similarity index 100% rename from node/src/infrastructure/ports/logger.ts rename to backend/node/src/infrastructure/ports/logger.ts diff --git a/node/src/infrastructure/ports/redis.ts b/backend/node/src/infrastructure/ports/redis.ts similarity index 100% rename from node/src/infrastructure/ports/redis.ts rename to backend/node/src/infrastructure/ports/redis.ts diff --git a/node/src/infrastructure/ports/s3.ts b/backend/node/src/infrastructure/ports/s3.ts similarity index 100% rename from node/src/infrastructure/ports/s3.ts rename to backend/node/src/infrastructure/ports/s3.ts diff --git a/node/src/infrastructure/repositories/user.repository.ts b/backend/node/src/infrastructure/repositories/user.repository.ts similarity index 100% rename from node/src/infrastructure/repositories/user.repository.ts rename to backend/node/src/infrastructure/repositories/user.repository.ts diff --git a/node/src/infrastructure/server/express/app.ts b/backend/node/src/infrastructure/server/express/app.ts similarity index 100% rename from node/src/infrastructure/server/express/app.ts rename to backend/node/src/infrastructure/server/express/app.ts diff --git a/node/src/infrastructure/server/express/handlers/app.handler.ts b/backend/node/src/infrastructure/server/express/handlers/app.handler.ts similarity index 100% rename from node/src/infrastructure/server/express/handlers/app.handler.ts rename to backend/node/src/infrastructure/server/express/handlers/app.handler.ts diff --git a/node/src/infrastructure/server/express/handlers/user.handler.ts b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts similarity index 100% rename from node/src/infrastructure/server/express/handlers/user.handler.ts rename to backend/node/src/infrastructure/server/express/handlers/user.handler.ts diff --git a/node/src/infrastructure/server/express/index.ts b/backend/node/src/infrastructure/server/express/index.ts similarity index 100% rename from node/src/infrastructure/server/express/index.ts rename to backend/node/src/infrastructure/server/express/index.ts diff --git a/node/src/infrastructure/server/express/middlewares/error.ts b/backend/node/src/infrastructure/server/express/middlewares/error.ts similarity index 100% rename from node/src/infrastructure/server/express/middlewares/error.ts rename to backend/node/src/infrastructure/server/express/middlewares/error.ts diff --git a/node/src/infrastructure/server/express/middlewares/logger.ts b/backend/node/src/infrastructure/server/express/middlewares/logger.ts similarity index 100% rename from node/src/infrastructure/server/express/middlewares/logger.ts rename to backend/node/src/infrastructure/server/express/middlewares/logger.ts diff --git a/node/src/infrastructure/server/express/middlewares/unique-user-email.ts b/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts similarity index 100% rename from node/src/infrastructure/server/express/middlewares/unique-user-email.ts rename to backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts diff --git a/node/src/infrastructure/server/express/middlewares/validate-uuid.ts b/backend/node/src/infrastructure/server/express/middlewares/validate-uuid.ts similarity index 100% rename from node/src/infrastructure/server/express/middlewares/validate-uuid.ts rename to backend/node/src/infrastructure/server/express/middlewares/validate-uuid.ts diff --git a/node/src/infrastructure/server/express/middlewares/validator.ts b/backend/node/src/infrastructure/server/express/middlewares/validator.ts similarity index 100% rename from node/src/infrastructure/server/express/middlewares/validator.ts rename to backend/node/src/infrastructure/server/express/middlewares/validator.ts diff --git a/node/src/infrastructure/server/express/package.json b/backend/node/src/infrastructure/server/express/package.json similarity index 100% rename from node/src/infrastructure/server/express/package.json rename to backend/node/src/infrastructure/server/express/package.json diff --git a/node/src/infrastructure/server/express/pnpm-lock.yaml b/backend/node/src/infrastructure/server/express/pnpm-lock.yaml similarity index 100% rename from node/src/infrastructure/server/express/pnpm-lock.yaml rename to backend/node/src/infrastructure/server/express/pnpm-lock.yaml diff --git a/node/src/infrastructure/server/express/routes/app.route.ts b/backend/node/src/infrastructure/server/express/routes/app.route.ts similarity index 87% rename from node/src/infrastructure/server/express/routes/app.route.ts rename to backend/node/src/infrastructure/server/express/routes/app.route.ts index 342bcb9..9d36dd4 100644 --- a/node/src/infrastructure/server/express/routes/app.route.ts +++ b/backend/node/src/infrastructure/server/express/routes/app.route.ts @@ -6,7 +6,7 @@ import { home } from '../handlers/app.handler'; const appRouter = Router(); -const openapi = readFileSync('../openapi.yaml', 'utf8'); +const openapi = readFileSync('../../openapi.yaml', 'utf8'); const swaggerDoc = parse(openapi); appRouter.get('/', home); diff --git a/node/src/infrastructure/server/express/routes/index.ts b/backend/node/src/infrastructure/server/express/routes/index.ts similarity index 100% rename from node/src/infrastructure/server/express/routes/index.ts rename to backend/node/src/infrastructure/server/express/routes/index.ts diff --git a/node/src/infrastructure/server/express/routes/user.route.ts b/backend/node/src/infrastructure/server/express/routes/user.route.ts similarity index 100% rename from node/src/infrastructure/server/express/routes/user.route.ts rename to backend/node/src/infrastructure/server/express/routes/user.route.ts diff --git a/node/src/infrastructure/server/express/schemas/auth.schema.ts b/backend/node/src/infrastructure/server/express/schemas/auth.schema.ts similarity index 100% rename from node/src/infrastructure/server/express/schemas/auth.schema.ts rename to backend/node/src/infrastructure/server/express/schemas/auth.schema.ts diff --git a/node/src/infrastructure/server/express/schemas/common.schema.ts b/backend/node/src/infrastructure/server/express/schemas/common.schema.ts similarity index 100% rename from node/src/infrastructure/server/express/schemas/common.schema.ts rename to backend/node/src/infrastructure/server/express/schemas/common.schema.ts diff --git a/node/src/infrastructure/server/express/schemas/user.schema.ts b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts similarity index 100% rename from node/src/infrastructure/server/express/schemas/user.schema.ts rename to backend/node/src/infrastructure/server/express/schemas/user.schema.ts diff --git a/node/src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts b/backend/node/src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts similarity index 100% rename from node/src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts rename to backend/node/src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts diff --git a/node/src/infrastructure/server/express/types/express/index.d.ts b/backend/node/src/infrastructure/server/express/types/express/index.d.ts similarity index 100% rename from node/src/infrastructure/server/express/types/express/index.d.ts rename to backend/node/src/infrastructure/server/express/types/express/index.d.ts diff --git a/node/src/infrastructure/services/file.service.ts b/backend/node/src/infrastructure/services/file.service.ts similarity index 100% rename from node/src/infrastructure/services/file.service.ts rename to backend/node/src/infrastructure/services/file.service.ts diff --git a/node/src/infrastructure/services/hash.service.ts b/backend/node/src/infrastructure/services/hash.service.ts similarity index 100% rename from node/src/infrastructure/services/hash.service.ts rename to backend/node/src/infrastructure/services/hash.service.ts diff --git a/node/src/infrastructure/services/redis.service.ts b/backend/node/src/infrastructure/services/redis.service.ts similarity index 100% rename from node/src/infrastructure/services/redis.service.ts rename to backend/node/src/infrastructure/services/redis.service.ts diff --git a/node/tsconfig.base.json b/backend/node/tsconfig.base.json similarity index 100% rename from node/tsconfig.base.json rename to backend/node/tsconfig.base.json diff --git a/node/tsconfig.eslint.json b/backend/node/tsconfig.eslint.json similarity index 100% rename from node/tsconfig.eslint.json rename to backend/node/tsconfig.eslint.json diff --git a/node/tsconfig.json b/backend/node/tsconfig.json similarity index 100% rename from node/tsconfig.json rename to backend/node/tsconfig.json diff --git a/backend/node/tsconfig.tsnode.json b/backend/node/tsconfig.tsnode.json new file mode 100644 index 0000000..d53ed09 --- /dev/null +++ b/backend/node/tsconfig.tsnode.json @@ -0,0 +1,106 @@ +{ + "compilerOptions": { + "strict": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "isolatedModules": true, + "checkJs": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "es2023" + ], + "module": "node16", + "target": "es2022", + "moduleResolution": "node16", + "baseUrl": "./", + "outDir": "./dist", + "incremental": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "removeComments": true, + "typeRoots": [ + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/types", + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/node_modules/@types", + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/types", + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/node_modules/@types", + "/home/husen/Projects/playground/backend/node/node_modules/@types" + ] + }, + "files": [ + "./src/adapters/controllers/create-user.controller.ts", + "./src/adapters/controllers/find-all-users.controller.ts", + "./src/adapters/controllers/find-one-user.controller.ts", + "./src/adapters/controllers/home.controller.ts", + "./src/adapters/controllers/remove-user.controller.ts", + "./src/adapters/controllers/unique-user-email.controller.ts", + "./src/adapters/controllers/update-user.controller.ts", + "./src/adapters/controllers/validate-uuid.controller.ts", + "./src/adapters/interfaces/common.interface.ts", + "./src/adapters/interfaces/http.interface.ts", + "./src/adapters/interfaces/user.interface.ts", + "./src/core/entities/common.entity.ts", + "./src/core/entities/user.entity.ts", + "./src/core/exceptions/bad-request.exception.ts", + "./src/core/exceptions/http.exception.ts", + "./src/core/exceptions/not-found.exception.ts", + "./src/core/interfaces/common.interface.ts", + "./src/core/interfaces/file.interface.ts", + "./src/core/interfaces/hash.interface.ts", + "./src/core/interfaces/http.interface.ts", + "./src/core/interfaces/redis.interface.ts", + "./src/core/interfaces/user.interface.ts", + "./src/core/use-cases/create-user.use-case.ts", + "./src/core/use-cases/find-all-users.use-case.ts", + "./src/core/use-cases/find-one-user.use-case.ts", + "./src/core/use-cases/remove-user.use-case.ts", + "./src/core/use-cases/unique-user-email.use-case.ts", + "./src/core/use-cases/update-user.use-case.ts", + "./src/core/use-cases/validate-uuid.use-case.ts", + "./src/infrastructure/config/app.ts", + "./src/infrastructure/config/auth.ts", + "./src/infrastructure/config/database.ts", + "./src/infrastructure/config/redis.ts", + "./src/infrastructure/config/s3.ts", + "./src/infrastructure/ports/database.ts", + "./src/infrastructure/ports/logger.ts", + "./src/infrastructure/ports/redis.ts", + "./src/infrastructure/ports/s3.ts", + "./src/infrastructure/repositories/user.repository.ts", + "./src/infrastructure/server/express/app.ts", + "./src/infrastructure/server/express/index.ts", + "./src/infrastructure/server/express/handlers/app.handler.ts", + "./src/infrastructure/server/express/handlers/user.handler.ts", + "./src/infrastructure/server/express/middlewares/error.ts", + "./src/infrastructure/server/express/middlewares/logger.ts", + "./src/infrastructure/server/express/middlewares/unique-user-email.ts", + "./src/infrastructure/server/express/middlewares/validate-uuid.ts", + "./src/infrastructure/server/express/middlewares/validator.ts", + "./src/infrastructure/server/express/routes/app.route.ts", + "./src/infrastructure/server/express/routes/index.ts", + "./src/infrastructure/server/express/routes/user.route.ts", + "./src/infrastructure/server/express/schemas/auth.schema.ts", + "./src/infrastructure/server/express/schemas/common.schema.ts", + "./src/infrastructure/server/express/schemas/user.schema.ts", + "./src/infrastructure/server/express/types/express/index.d.ts", + "./src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts", + "./src/infrastructure/services/file.service.ts", + "./src/infrastructure/services/hash.service.ts", + "./src/infrastructure/services/redis.service.ts" + ], + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/node/vitest.config.ts b/backend/node/vitest.config.ts similarity index 100% rename from node/vitest.config.ts rename to backend/node/vitest.config.ts diff --git a/config/kratos/email-password/identity.schema.json b/config/kratos/email-password/identity.schema.json new file mode 100644 index 0000000..1a13787 --- /dev/null +++ b/config/kratos/email-password/identity.schema.json @@ -0,0 +1,49 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/config/kratos/email-password/kratos.yml b/config/kratos/email-password/kratos.yml new file mode 100644 index 0000000..4ca37f9 --- /dev/null +++ b/config/kratos/email-password/kratos.yml @@ -0,0 +1,102 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + allowed_return_urls: + - http://127.0.0.1:4455 + - http://localhost:19006/Callback + - exp://localhost:8081/--/Callback + + methods: + password: + enabled: true + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + use: code + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verification + use: code + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/login + + login: + ui_url: http://127.0.0.1:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/registration + after: + password: + hooks: + - hook: session + - hook: show_verification_ui + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: default + schemas: + - id: default + url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true + +# feature_flags: +# use_continue_with_transitions: true diff --git a/config/mailhog/auth b/config/mailhog/auth new file mode 100644 index 0000000..4e0df0f --- /dev/null +++ b/config/mailhog/auth @@ -0,0 +1 @@ +test:$2a$04$qxRo.ftFoNep7ld/5jfKtuBTnGqff/fZVyj53mUC5sVf9dtDLAi/S diff --git a/config/oathkeeper/access-rules.yml b/config/oathkeeper/access-rules.yml new file mode 100644 index 0000000..fb6e613 --- /dev/null +++ b/config/oathkeeper/access-rules.yml @@ -0,0 +1,60 @@ +- + id: "ory:kratos:public" + upstream: + preserve_host: true + url: "http://kratos:4433" + strip_path: /.ory/kratos/public + match: + url: "http://127.0.0.1:4455/.ory/kratos/public/<**>" + methods: + - GET + - POST + - PUT + - DELETE + - PATCH + authenticators: + - + handler: noop + authorizer: + handler: allow + mutators: + - handler: noop + +- + id: "ory:kratos-selfservice-ui-node:anonymous" + upstream: + preserve_host: true + url: "http://kratos-selfservice-ui-node:4435" + match: + url: "http://127.0.0.1:4455/<{registration,welcome,recovery,verification,login,error,health/{alive,ready},**.css,**.js,**.png,**.svg,**.woff*}>" + methods: + - GET + authenticators: + - + handler: anonymous + authorizer: + handler: allow + mutators: + - + handler: noop + +- + id: "ory:kratos-selfservice-ui-node:protected" + upstream: + preserve_host: true + url: "http://kratos-selfservice-ui-node:4435" + match: + url: "http://127.0.0.1:4455/<{sessions,settings}>" + methods: + - GET + authenticators: + - + handler: cookie_session + authorizer: + handler: allow + mutators: + - handler: id_token + errors: + - handler: redirect + config: + to: http://127.0.0.1:4455/login diff --git a/config/oathkeeper/id_token.jwks.json b/config/oathkeeper/id_token.jwks.json new file mode 100644 index 0000000..5bc1ec1 --- /dev/null +++ b/config/oathkeeper/id_token.jwks.json @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "a2aa9739-d753-4a0d-87ee-61f101050277", + "alg": "RS256", + "n": "zpjSl0ySsdk_YC4ZJYYV-cSznWkzndTo0lyvkYmeBkW60YHuHzXaviHqonY_DjFBdnZC0Vs_QTWmBlZvPzTp4Oni-eOetP-Ce3-B8jkGWpKFOjTLw7uwR3b3jm_mFNiz1dV_utWiweqx62Se0SyYaAXrgStU8-3P2Us7_kz5NnBVL1E7aEP40aB7nytLvPhXau-YhFmUfgykAcov0QrnNY0DH0eTcwL19UysvlKx6Uiu6mnbaFE1qx8X2m2xuLpErfiqj6wLCdCYMWdRTHiVsQMtTzSwuPuXfH7J06GTo3I1cEWN8Mb-RJxlosJA_q7hEd43yYisCO-8szX0lgCasw", + "e": "AQAB", + "d": "x3dfY_rna1UQTmFToBoMn6Edte47irhkra4VSNPwwaeTTvI-oN2TO51td7vo91_xD1nw-0c5FFGi4V2UfRcudBv9LD1rHt_O8EPUh7QtAUeT3_XXgjx1Xxpqu5goMZpkTyGZ-B6JzOY3L8lvWQ_Qeia1EXpvxC-oTOjJnKZeuwIPlcoNKMRU-mIYOnkRFfnUvrDm7N9UZEp3PfI3vhE9AquP1PEvz5KTUYkubsfmupqqR6FmMUm6ulGT7guhBw9A3vxIYbYGKvXLdBvn68mENrEYxXrwmu6ITMh_y208M5rC-hgEHIAIvMu1aVW6jNgyQTunsGST3UyrSbwjI0K9UQ", + "p": "77fDvnfHRFEgyi7mh0c6fAdtMEMJ05W8NwTG_D-cSwfWipfTwJJrroWoRwEgdAg5AWGq-MNUzrubTVXoJdC2T4g1o-VRZkKKYoMvav3CvOIMzCBxBs9I_GAKr5NCSk7maksMqiCTMhmkoZ5RPuMYMY_YzxKNAbjBd9qFLfaVAqs", + "q": "3KEmPA2XQkf7dvtpY1Xkp1IfMV_UBdmYk7J6dB5BYqzviQWdEFvWaSATJ_7qV1dw0JDZynOgipp8gvoL-RepfjtArhPz41wB3J2xmBYrBr1sJ-x5eqAvMkQk2bd5KTor44e79TRIkmkFYAIdUQ5JdVXPA13S8WUZfb_bAbwaCBk", + "dp": "5uyy32AJkNFKchqeLsE6INMSp0RdSftbtfCfM86fZFQno5lA_qjOnO_avJPkTILDT4ZjqoKYxxJJOEXCffNCPPltGvbE5GrDXsUbP8k2-LgWNeoml7XFjIGEqcCFQoohQ1IK4DTDN6cmRh76C0e_Pbdh15D6TydJEIlsdGuu_kM", + "dq": "aegFNYCEojFxeTzX6vIZL2RRSt8oJKK-Be__reu0EUzYMtr5-RdMhev6phFMph54LfXKRc9ZOg9MQ4cJ5klAeDKzKpyzTukkj6U20b2aa8LTvxpZec6YuTVSxxu2Ul71IGRQijTNvVIiXWLGddk409Ub6Q7JqkyQfvdwhpWnnUk", + "qi": "P68-EwgcRy9ce_PZ75c909cU7dzCiaGcTX1psJiXmQAFBcG0msWfsyHGbllOZG27pKde78ORGJDYDNk1FqTwsogZyCP87EiBmOoqXWnMvKYfJ1DOx7x42LMAGwMD3bgQj9jgRACxFJG4n3NI6uFlFruyl_CLQzwW_rQFHshLK7Q" + } + ] +} diff --git a/config/oathkeeper/oathkeeper.yml b/config/oathkeeper/oathkeeper.yml new file mode 100644 index 0000000..ff8ec39 --- /dev/null +++ b/config/oathkeeper/oathkeeper.yml @@ -0,0 +1,88 @@ +log: + level: debug + format: json + +serve: + proxy: + cors: + enabled: true + allowed_origins: + - "*" + allowed_methods: + - POST + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Content-Type + exposed_headers: + - Content-Type + allow_credentials: true + debug: true + +errors: + fallback: + - json + + handlers: + redirect: + enabled: true + config: + to: http://127.0.0.1:4455/login + when: + - + error: + - unauthorized + - forbidden + request: + header: + accept: + - text/html + json: + enabled: true + config: + verbose: true + +access_rules: + matching_strategy: glob + repositories: + - file:///etc/config/oathkeeper/access-rules.yml + +authenticators: + anonymous: + enabled: true + config: + subject: guest + + cookie_session: + enabled: true + config: + check_session_url: http://kratos:4433/sessions/whoami + preserve_path: true + extra_from: "@this" + subject_from: "identity.id" + only: + - ory_kratos_session + + noop: + enabled: true + +authorizers: + allow: + enabled: true + +mutators: + noop: + enabled: true + + id_token: + enabled: true + config: + issuer_url: http://127.0.0.1:4455/ + jwks_url: file:///etc/config/oathkeeper/id_token.jwks.json + claims: | + { + "session": {{ .Extra | toJson }} + } diff --git a/docker-compose-express.yml b/docker-compose-express.yml new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose-gateway.yml b/docker-compose-gateway.yml new file mode 100644 index 0000000..e76f915 --- /dev/null +++ b/docker-compose-gateway.yml @@ -0,0 +1,84 @@ +version: '3.8' + +services: + kratos-migrate: + image: docker.io/oryd/kratos:v1.0.0 + depends_on: + - postgres + environment: + - DSN=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./config/kratos/email-password + target: /etc/config/kratos + command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes + restart: on-failure + networks: + - playground + + kratos: + image: docker.io/oryd/kratos:v1.0.0 + depends_on: + - kratos-migrate + ports: + - '4433:4433' # public + - '4434:4434' # admin + restart: unless-stopped + environment: + - DSN=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 + - LOG_LEVEL=trace + - SERVE_PUBLIC_BASE_URL=http://127.0.0.1:4455/.ory/kratos/public/ + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./config/kratos/email-password + target: /etc/config/kratos + networks: + - playground + + kratos-selfservice-ui-node: + image: docker.io/oryd/kratos-selfservice-ui-node:v0.13.0-20 + depends_on: + - kratos + restart: on-failure + ports: + - 4435:4435 + environment: + - PORT=4435 + - KRATOS_BROWSER_URL=http://127.0.0.1:4455/.ory/kratos/public + - JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json + - SECURITY_MODE=jwks + - COOKIE_SECRET=playground12345 + - CSRF_COOKIE_NAME=__locahost-example.com-x-csrf-token + - CSRF_COOKIE_SECRET=playground12345 + - KRATOS_PUBLIC_URL=http://kratos:4433/ + networks: + - playground + + oathkeeper: + image: docker.io/oryd/oathkeeper:v0.40.6 + depends_on: + - kratos + ports: + - 4455:4455 + - 4456:4456 + command: + serve proxy -c "/etc/config/oathkeeper/oathkeeper.yml" + environment: + - LOG_LEVEL=debug + restart: on-failure + networks: + - playground + volumes: + - ./config/oathkeeper:/etc/config/oathkeeper + +volumes: + kratos-sqlite: diff --git a/docker-compose-sveltekit.yml b/docker-compose-sveltekit.yml new file mode 100644 index 0000000..e69de29 diff --git a/node/docker-compose.yml b/docker-compose.yml similarity index 74% rename from node/docker-compose.yml rename to docker-compose.yml index 5bbdc71..1b81758 100644 --- a/node/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ -version: '3' +version: '3.8' + services: postgres: image: docker.io/library/postgres:14-alpine @@ -23,6 +24,8 @@ services: ] retries: 3 timeout: 5s + networks: + - playground redis: image: docker.io/library/redis:6-alpine @@ -34,6 +37,8 @@ services: test: ['CMD', 'redis-cli', 'ping'] retries: 3 timeout: 5s + networks: + - playground minio: image: docker.io/minio/minio @@ -50,6 +55,8 @@ services: test: ['CMD', 'curl', '-f', 'http://127.0.0.1:9000/minio/health/live'] retries: 3 timeout: 5s + networks: + - playground minio-client: image: docker.io/minio/mc @@ -63,14 +70,32 @@ services: /usr/bin/mc policy set download myminio/local; exit 0; " + networks: + - playground + + # mailhog: + # image: docker.io/mailhog/mailhog:v1.0.0 + # ports: + # - '${MAILHOG_PORT:-1025}:1025' + # - '${MAILHOG_DASHBOARD_PORT:-8025}:8025' + # volumes: + # - ../config:/.config + # environment: + # - MH_AUTH_FILE="/.config/mailhog/auth" + # networks: + # - playground - mailhog: - image: mailhog/mailhog:v1.0.0 + mailslurper: + image: docker.io/oryd/mailslurper:latest-smtps ports: - - '${MAILHOG_PORT:-1025}:1025' - - '${MAILHOG_DASHBOARD_PORT:-8025}:8025' + - '4436:4436' + - '4437:4437' + networks: + - playground volumes: postgres-data: redis-data: minio-data: +networks: + playground: diff --git a/node/tsconfig.tsnode.json b/node/tsconfig.tsnode.json deleted file mode 100644 index 4776910..0000000 --- a/node/tsconfig.tsnode.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "allowUnusedLabels": false, - "allowUnreachableCode": false, - "exactOptionalPropertyTypes": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "isolatedModules": true, - "checkJs": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "lib": ["es2023"], - "module": "node16", - "target": "es2022", - "moduleResolution": "node16", - "baseUrl": "./", - "outDir": "./dist", - "incremental": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "removeComments": true, - "typeRoots": [ - "/home/husen/Projects/templates/node/src/infrastructure/server/express/types", - "/home/husen/Projects/templates/node/src/infrastructure/server/express/node_modules/@types", - "/home/husen/Projects/templates/node/src/infrastructure/server/fastify/types", - "/home/husen/Projects/templates/node/src/infrastructure/server/fastify/node_modules/@types", - "/home/husen/Projects/templates/node/node_modules/@types" - ] - }, - "files": [ - "./src/adapters/controllers/create-user.controller.ts", - "./src/adapters/controllers/find-all-users.controller.ts", - "./src/adapters/controllers/find-one-user.controller.ts", - "./src/adapters/controllers/home.controller.ts", - "./src/adapters/controllers/remove-user.controller.ts", - "./src/adapters/controllers/unique-user-email.controller.ts", - "./src/adapters/controllers/update-user.controller.ts", - "./src/adapters/controllers/validate-uuid.controller.ts", - "./src/adapters/interfaces/common.interface.ts", - "./src/adapters/interfaces/http.interface.ts", - "./src/adapters/interfaces/user.interface.ts", - "./src/core/entities/common.entity.ts", - "./src/core/entities/user.entity.ts", - "./src/core/exceptions/bad-request.exception.ts", - "./src/core/exceptions/http.exception.ts", - "./src/core/exceptions/not-found.exception.ts", - "./src/core/interfaces/common.interface.ts", - "./src/core/interfaces/file.interface.ts", - "./src/core/interfaces/hash.interface.ts", - "./src/core/interfaces/http.interface.ts", - "./src/core/interfaces/redis.interface.ts", - "./src/core/interfaces/user.interface.ts", - "./src/core/use-cases/create-user.use-case.ts", - "./src/core/use-cases/find-all-users.use-case.ts", - "./src/core/use-cases/find-one-user.use-case.ts", - "./src/core/use-cases/remove-user.use-case.ts", - "./src/core/use-cases/unique-user-email.use-case.ts", - "./src/core/use-cases/update-user.use-case.ts", - "./src/core/use-cases/validate-uuid.use-case.ts", - "./src/infrastructure/config/app.ts", - "./src/infrastructure/config/auth.ts", - "./src/infrastructure/config/database.ts", - "./src/infrastructure/config/redis.ts", - "./src/infrastructure/config/s3.ts", - "./src/infrastructure/ports/database.ts", - "./src/infrastructure/ports/logger.ts", - "./src/infrastructure/ports/redis.ts", - "./src/infrastructure/ports/s3.ts", - "./src/infrastructure/repositories/user.repository.ts", - "./src/infrastructure/server/express/app.ts", - "./src/infrastructure/server/express/index.ts", - "./src/infrastructure/server/express/handlers/app.handler.ts", - "./src/infrastructure/server/express/handlers/user.handler.ts", - "./src/infrastructure/server/express/middlewares/error.ts", - "./src/infrastructure/server/express/middlewares/logger.ts", - "./src/infrastructure/server/express/middlewares/unique-user-email.ts", - "./src/infrastructure/server/express/middlewares/validate-uuid.ts", - "./src/infrastructure/server/express/middlewares/validator.ts", - "./src/infrastructure/server/express/routes/app.route.ts", - "./src/infrastructure/server/express/routes/index.ts", - "./src/infrastructure/server/express/routes/user.route.ts", - "./src/infrastructure/server/express/schemas/auth.schema.ts", - "./src/infrastructure/server/express/schemas/common.schema.ts", - "./src/infrastructure/server/express/schemas/user.schema.ts", - "./src/infrastructure/server/express/types/express/index.d.ts", - "./src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts", - "./src/infrastructure/services/file.service.ts", - "./src/infrastructure/services/hash.service.ts", - "./src/infrastructure/services/redis.service.ts" - ], - "include": ["./src/**/*.ts"], - "exclude": ["./src/**/*.spec.ts"] -} diff --git a/openapi.yaml b/openapi.yaml index f364fa2..5a377a4 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1,181 +1,21 @@ # yaml-language-server: $schema=https://github.com/OAI/OpenAPI-Specification/raw/main/schemas/v3.0/schema.json -openapi: 3.0.3 +openapi: 3.1.0 info: - title: My REST API template + title: Playground REST API description: > - A collections of my REST API template for different language. This template - follows the [JSON:API](https://jsonapi.org/) specification, unless there is - a specific endpoint specification. + A collections of Playground REST API template for different language. This + template follows the [JSON:API](https://jsonapi.org/) specification, unless + there is a specific endpoint specification. contact: - name: Ahmad Husen + name: Husen url: https://husen.id email: hi@husen.id license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html - version: 0.1.0 + version: 0.2.0 paths: - /auth/login: - post: - tags: - - "auth" - summary: Log in to the site. - description: > - This endpoint follow Bearer Token Usage specification - [[RFC 6750]](https://tools.ietf.org/html/rfc6750). - operationId: logIn - requestBody: - content: - "application/json": - schema: - type: object - properties: - username: - type: string - password: - type: string - required: - - "username" - - "password" - example: - username: johndoe@example.com - password: abogoboga - responses: - "200": - $ref: "#/components/responses/BearerAuth" - "400": - description: Invalid request body. - content: - "application/vnd.api+json": - schema: - allOf: - - $ref: "#/components/schemas/Error" - - example: - links: - self: "/auth/login" - errors: - - status: 400 - title: Bad Request - description: The username is required. - source: - pointer: "/data/attributes/username" - - status: 400 - title: Bad Request - description: The password is required. - source: - pointer: "/data/attributes/password" - "401": - description: Invalid credentials. - content: - "application/vnd.api+json": - schema: - allOf: - - $ref: "#/components/schemas/Error" - - example: - links: - self: "/auth/login" - errors: - - status: 401 - title: Unauthorized - description: These credentials do not match our records. - "500": - description: Internal server error. - content: - "application/vnd.api+json": - schema: - allOf: - - $ref: "#/components/schemas/InternalServerError" - - example: - links: - self: "/auth/login" - security: - - {} - /auth/profile: - get: - tags: - - "auth" - summary: Get current logged in user. - operationId: getProfile - responses: - "200": - description: "Return current logged in user." - content: - "application/vnd.api+json": - schema: - allOf: - - $ref: "#/components/schemas/SingleUserResponseData" - - example: - links: - self: "/auth/profile" - "400": - $ref: "#/components/responses/InvalidRequest" - "401": - $ref: "#/components/responses/InvalidToken" - "500": - description: Internal server error. - content: - "application/vnd.api+json": - schema: - allOf: - - $ref: "#/components/schemas/InternalServerError" - - example: - links: - self: "/auth/profile" - /auth/refresh: - get: - tags: - - "auth" - summary: Refresh access and refresh token. - description: > - Currently not working automatically due limitation of Swagger UI. - operationId: refresh - responses: - "200": - $ref: "#/components/responses/BearerAuth" - "400": - $ref: "#/components/responses/InvalidRequest" - "401": - $ref: "#/components/responses/InvalidToken" - "500": - description: Internal server error. - content: - "application/vnd.api+json": - schema: - allOf: - - $ref: "#/components/schemas/InternalServerError" - - example: - links: - self: "/auth/logout" - security: - - cookieAuth: [] - /auth/logout: - post: - tags: - - "auth" - summary: Log out current user. - description: > - Currently not working automatically due limitation of Swagger UI. - operationId: logOut - responses: - "204": - description: "User log out successfully." - "400": - $ref: "#/components/responses/InvalidRequest" - "401": - $ref: "#/components/responses/InvalidToken" - "500": - description: Internal server error. - content: - "application/vnd.api+json": - schema: - allOf: - - $ref: "#/components/schemas/InternalServerError" - - example: - links: - self: "/auth/logout" - security: - - cookieAuth: [] /user: post: tags: @@ -212,8 +52,6 @@ paths: - example: links: self: "/user" - "401": - $ref: "#/components/responses/InvalidToken" "500": description: Internal server error. content: @@ -240,10 +78,6 @@ paths: - example: links: self: "/user" - "400": - $ref: "#/components/responses/InvalidRequest" - "401": - $ref: "#/components/responses/InvalidToken" "500": description: Internal server error. content: @@ -288,8 +122,6 @@ paths: - example: links: self: "/user/{id}" - "401": - $ref: "#/components/responses/InvalidToken" "404": $ref: "#/components/responses/UserNotFound" "500": @@ -345,8 +177,6 @@ paths: - example: links: self: "/user/{id}" - "401": - $ref: "#/components/responses/InvalidToken" "404": $ref: "#/components/responses/UserNotFound" "500": @@ -384,8 +214,6 @@ paths: - example: links: self: "/user/{id}" - "401": - $ref: "#/components/responses/InvalidToken" "404": $ref: "#/components/responses/UserNotFound" "500": @@ -468,13 +296,6 @@ components: detail: Validation failed (uuid v4 is expected). source: parameter: id - BearerAuthError: - type: object - properties: - error: - type: string - error_description: - type: string User: type: object properties: @@ -640,55 +461,6 @@ components: source: parameter: id responses: - InvalidRequest: - description: The token is missing or malformed. - headers: - "WWW-Authenticate": - $ref: "#/components/headers/bearerAuthError" - content: - "application/json": - schema: - allOf: - - $ref: "#/components/schemas/BearerAuthError" - - example: - error: invalid_request - error_description: The token is missing or malformed. - InvalidToken: - description: The token is revoked or expired. - headers: - "WWW-Authenticate": - $ref: "#/components/headers/bearerAuthError" - content: - "application/json": - schema: - allOf: - - $ref: "#/components/schemas/BearerAuthError" - - example: - error: invalid_token - error_description: The token has been revoked or expired. - BearerAuth: - description: > - Valid credentials, with a response format that follows - [RFC6750](https://datatracker.ietf.org/doc/html/rfc6750#section-4). - content: - "application/json": - schema: - type: object - properties: - access_token: - type: string - token_type: - type: string - default: Bearer - expires_in: - type: integer - refresh_token: - type: string - example: - access_token: - token_type: Bearer - expires_in: 3600 - refresh_token: UserNotFound: description: User not found, usually caused by invalid id parameter. content: @@ -705,25 +477,3 @@ components: detail: The user is not found. source: parameter: id - - headers: - bearerAuthError: - description: > - HTTP authentication methods ("challenges") that might be used. - schema: - type: string - example: Bearer realm="Access to the site" - securitySchemes: - bearerAuth: - description: JWT based access token authentication - type: http - scheme: bearer - bearerFormat: JWT - cookieAuth: - description: JWT based refresh token authentication - type: apiKey - in: cookie - name: refresh-token -security: - - bearerAuth: [] - From 112fd425462ac8b201abbffd32f2619e6ef6d4c3 Mon Sep 17 00:00:00 2001 From: Husen Date: Sun, 17 Dec 2023 16:12:19 +0700 Subject: [PATCH 2/8] chore: remove password from app Signed-off-by: Husen --- .../node/__tests__/fixtures/user.fixture.ts | 9 +- backend/node/__tests__/user.test.ts | 442 ++---------------- .../20220530152859_create_users_tables.js | 4 +- backend/node/database/seeds/users.js | 5 +- .../create-user.controller.spec.ts | 33 +- .../controllers/create-user.controller.ts | 16 +- .../find-all-users.controller.spec.ts | 27 +- .../controllers/find-all-users.controller.ts | 8 +- .../find-one-user.controller.spec.ts | 23 +- .../unique-user-email.controller.spec.ts | 5 +- .../update-user.controller.spec.ts | 45 +- .../controllers/update-user.controller.ts | 10 +- .../src/adapters/interfaces/http.interface.ts | 13 - backend/node/src/core/entities/user.entity.ts | 3 +- .../core/exceptions/bad-request.exception.ts | 6 +- .../src/core/interfaces/hash.interface.ts | 4 - .../src/core/interfaces/user.interface.ts | 11 +- .../use-cases/create-user.use-case.spec.ts | 33 +- .../core/use-cases/create-user.use-case.ts | 14 +- .../use-cases/find-all-users.use-case.spec.ts | 22 +- .../core/use-cases/find-all-users.use-case.ts | 24 +- .../use-cases/find-one-user.use-case.spec.ts | 25 +- .../core/use-cases/find-one-user.use-case.ts | 9 +- .../use-cases/remove-user.use-case.spec.ts | 14 +- .../unique-user-email.use-case.spec.ts | 5 +- .../use-cases/update-user.use-case.spec.ts | 92 +--- .../core/use-cases/update-user.use-case.ts | 24 +- .../repositories/user.repository.ts | 36 +- .../server/express/handlers/user.handler.ts | 9 +- .../server/express/middlewares/error.ts | 19 +- .../server/express/schemas/user.schema.ts | 74 +-- .../infrastructure/services/hash.service.ts | 16 - backend/node/tsconfig.tsnode.json | 200 ++++---- ...pose-gateway.yml => docker-compose-ory.yml | 0 docker-compose.yml | 4 +- openapi.yaml | 61 +-- shell.nix | 4 + 37 files changed, 427 insertions(+), 922 deletions(-) delete mode 100644 backend/node/src/core/interfaces/hash.interface.ts delete mode 100644 backend/node/src/infrastructure/services/hash.service.ts rename docker-compose-gateway.yml => docker-compose-ory.yml (100%) diff --git a/backend/node/__tests__/fixtures/user.fixture.ts b/backend/node/__tests__/fixtures/user.fixture.ts index 33f46e1..4b733dc 100644 --- a/backend/node/__tests__/fixtures/user.fixture.ts +++ b/backend/node/__tests__/fixtures/user.fixture.ts @@ -1,19 +1,19 @@ import type { UserTable } from '../../src/core/interfaces/user.interface'; import { userRepository } from '../../src/infrastructure/repositories/user.repository'; -import { hashService } from '../../src/infrastructure/services/hash.service'; export const user: Omit = { id: 'c9b1dbe1-6eb7-463e-bfa5-4733d195bad9', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', email: 'johndoe@example.com', - password: 'abogoboga', }; const { id, ...tempUser } = user; export const testUser: Omit = { ...tempUser, - name: 'Jane Doe', + first_name: 'Jane', + last_name: 'Doe', email: 'janedoe@example.com', }; @@ -22,6 +22,5 @@ export async function setup() { await userRepository.create({ ...user, - password: await hashService.create(user.password), }); } diff --git a/backend/node/__tests__/user.test.ts b/backend/node/__tests__/user.test.ts index 3935707..daae262 100644 --- a/backend/node/__tests__/user.test.ts +++ b/backend/node/__tests__/user.test.ts @@ -33,23 +33,13 @@ describe('POST /user', () => { { status: 400, title: 'Bad Request', - detail: 'The name is required.', + detail: 'The first name is required.', }, { status: 400, title: 'Bad Request', detail: 'The email is required.', }, - { - status: 400, - title: 'Bad Request', - detail: 'The password is required.', - }, - { - status: 400, - title: 'Bad Request', - detail: 'The password confirmation is required.', - }, ], }); }); @@ -59,10 +49,9 @@ describe('POST /user', () => { .post('/user') .set('Accept', 'application/vnd.api+json') .send({ - name: user.name, + first_name: user.first_name, + last_name: user.last_name, email: 'johndoe', - password: user.password, - password_confirmation: user.password, }); expect(response.status).toEqual(400); @@ -83,78 +72,14 @@ describe('POST /user', () => { }); }); - test('should return error when password is invalid', async () => { - const response = await request - .post('/user') - .set('Accept', 'application/vnd.api+json') - .send({ - name: user.name, - email: user.email, - password: 'abogo', - password_confirmation: 'abogo', - }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: '/user', - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password must be at least 8 characters.', - }, - { - status: 400, - title: 'Bad Request', - detail: 'The password confirmation must be at least 8 characters.', - }, - ], - }); - }); - - test('should return error when password confirmation is invalid', async () => { - const response = await request - .post('/user') - .set('Accept', 'application/vnd.api+json') - .send({ - name: user.name, - email: user.email, - password: user.password, - password_confirmation: 'abogobog', - }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: '/user', - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password confirmation does not match.', - }, - ], - }); - }); - test('should return error when email is already taken', async () => { const response = await request .post('/user') .set('Accept', 'application/vnd.api+json') .send({ - name: user.name, + first_name: user.first_name, + last_name: user.last_name, email: user.email, - password: user.password, - password_confirmation: user.password, }); expect(response.status).toEqual(400); @@ -180,10 +105,9 @@ describe('POST /user', () => { .post('/user') .set('Accept', 'application/vnd.api+json') .send({ - name: testUser.name, + first_name: testUser.first_name, + last_name: testUser.last_name, email: testUser.email, - password: testUser.password, - password_confirmation: testUser.password, }); expect(response.status).toEqual(201); @@ -200,8 +124,12 @@ describe('POST /user', () => { expect(response.body).toHaveProperty('data.id'); expect(response.body).toHaveProperty('data.type', 'users'); expect(response.body).toHaveProperty( - 'data.attributes.name', - testUser.name + 'data.attributes.first_name', + testUser.first_name + ); + expect(response.body).toHaveProperty( + 'data.attributes.last_name', + testUser.last_name ); expect(response.body).toHaveProperty('data.attributes.nickname'); expect(response.body).toHaveProperty( @@ -234,8 +162,12 @@ describe('GET /user', () => { ); expect(response.body).toHaveProperty(['data', 0, 'type'], 'users'); expect(response.body).toHaveProperty( - ['data', 0, 'attributes', 'name'], - user.name + ['data', 0, 'attributes', 'first_name'], + user.first_name + ); + expect(response.body).toHaveProperty( + ['data', 0, 'attributes', 'last_name'], + user.last_name ); expect(response.body).toHaveProperty([ 'data', @@ -336,8 +268,12 @@ describe('GET /user/{id}', () => { expect(response.body).toHaveProperty('data.id', user.id); expect(response.body).toHaveProperty('data.type', 'users'); expect(response.body).toHaveProperty( - 'data.attributes.name', - user.name + 'data.attributes.first_name', + user.first_name + ); + expect(response.body).toHaveProperty( + 'data.attributes.last_name', + user.last_name ); expect(response.body).toHaveProperty('data.attributes.nickname'); expect(response.body).toHaveProperty( @@ -402,7 +338,7 @@ describe('PATCH /user/{id}', () => { const response = await request .patch(`/user/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ name: null }); + .send({ first_name: null }); expect(response.status).toEqual(400); expect(response.body).toEqual({ @@ -416,7 +352,7 @@ describe('PATCH /user/{id}', () => { { status: 400, title: 'Bad Request', - detail: 'The name must be a string.', + detail: 'The first name must be a string.', }, ], }); @@ -426,7 +362,7 @@ describe('PATCH /user/{id}', () => { const response = await request .patch(`/user/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ name: '' }); + .send({ first_name: '' }); expect(response.status).toEqual(400); expect(response.body).toEqual({ @@ -440,7 +376,7 @@ describe('PATCH /user/{id}', () => { { status: 400, title: 'Bad Request', - detail: 'The name field must have a value.', + detail: 'The first name field must have a value.', }, ], }); @@ -450,7 +386,9 @@ describe('PATCH /user/{id}', () => { const response = await request .patch(`/user/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ name: 12345 }); + .send({ + first_name: 12345, + }); expect(response.status).toEqual(400); expect(response.body).toEqual({ @@ -464,7 +402,7 @@ describe('PATCH /user/{id}', () => { { status: 400, title: 'Bad Request', - detail: 'The name must be a string.', + detail: 'The first name must be a string.', }, ], }); @@ -490,8 +428,12 @@ describe('PATCH /user/{id}', () => { expect(response.body).toHaveProperty('data.id', user.id); expect(response.body).toHaveProperty('data.type', 'users'); expect(response.body).toHaveProperty( - 'data.attributes.name', - user.name + 'data.attributes.first_name', + user.first_name + ); + expect(response.body).toHaveProperty( + 'data.attributes.last_name', + user.last_name ); expect(response.body).toHaveProperty( 'data.attributes.nickname', @@ -527,8 +469,12 @@ describe('PATCH /user/{id}', () => { expect(response.body).toHaveProperty('data.id', user.id); expect(response.body).toHaveProperty('data.type', 'users'); expect(response.body).toHaveProperty( - 'data.attributes.name', - user.name + 'data.attributes.first_name', + user.first_name + ); + expect(response.body).toHaveProperty( + 'data.attributes.last_name', + user.last_name ); expect(response.body).toHaveProperty( 'data.attributes.nickname', @@ -640,296 +586,6 @@ describe('PATCH /user/{id}', () => { }); }); - test('should return error when password is null', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password: null }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password must be a string.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password confirmation field is required when password is present.', - }, - ], - }); - }); - - test('should return error when password is empty', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password: '' }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password must be at least 8 characters.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password confirmation field is required when password is present.', - }, - ], - }); - }); - - test('should return error when password is invalid', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password: 12345 }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password must be a string.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password confirmation field is required when password is present.', - }, - ], - }); - }); - - test('should return error when password is invalid', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password: 'abogo' }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password must be at least 8 characters.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password confirmation field is required when password is present.', - }, - ], - }); - }); - - test('should return error when password is valid but password confirmation is missing', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password: 'abogobog' }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: - 'The password confirmation field is required when password is present.', - }, - ], - }); - }); - - test('should return error when password confirmation is null', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password_confirmation: null }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password confirmation must be a string.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password field is required when password confirmation is present.', - }, - ], - }); - }); - - test('should return error when password confirmation is empty', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password_confirmation: '' }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password confirmation must be at least 8 characters.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password field is required when password confirmation is present.', - }, - ], - }); - }); - - test('should return error when password confirmation is invalid', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password_confirmation: 12345 }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password confirmation must be a string.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password field is required when password confirmation is present.', - }, - ], - }); - }); - - test('should return error when password confirmation is invalid', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password_confirmation: 'abogo' }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: 'The password confirmation must be at least 8 characters.', - }, - { - status: 400, - title: 'Bad Request', - detail: - 'The password field is required when password confirmation is present.', - }, - ], - }); - }); - - test('should return error when password confirmation is valid but password is missing', async () => { - const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json') - .send({ password_confirmation: 'abogobog' }); - - expect(response.status).toEqual(400); - expect(response.body).toEqual({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - errors: [ - { - status: 400, - title: 'Bad Request', - detail: - 'The password field is required when password confirmation is present.', - }, - ], - }); - }); - test('should not update an user', async () => { const response = await request .patch(`/user/${user.id}`) @@ -949,8 +605,12 @@ describe('PATCH /user/{id}', () => { expect(response.body).toHaveProperty('data.id', user.id); expect(response.body).toHaveProperty('data.type', 'users'); expect(response.body).toHaveProperty( - 'data.attributes.name', - user.name + 'data.attributes.first_name', + user.first_name + ); + expect(response.body).toHaveProperty( + 'data.attributes.last_name', + user.last_name ); expect(response.body).toHaveProperty('data.attributes.nickname'); expect(response.body).toHaveProperty( diff --git a/backend/node/database/migrations/20220530152859_create_users_tables.js b/backend/node/database/migrations/20220530152859_create_users_tables.js index cd872a8..c58e856 100644 --- a/backend/node/database/migrations/20220530152859_create_users_tables.js +++ b/backend/node/database/migrations/20220530152859_create_users_tables.js @@ -11,11 +11,11 @@ exports.up = async function up(knex) { .notNullable() .defaultTo(knex.raw('uuid_generate_v4()')) .primary(); - table.string('name').notNullable(); + table.string('first_name').notNullable(); + table.string('last_name').nullable(); table.string('nickname').nullable(); table.string('email').notNullable().unique(); table.timestamp('email_verified_at').nullable(); - table.string('password').notNullable(); table.string('photo').nullable(); table.timestamps(true, true); }); diff --git a/backend/node/database/seeds/users.js b/backend/node/database/seeds/users.js index 0f05673..0e9aca1 100644 --- a/backend/node/database/seeds/users.js +++ b/backend/node/database/seeds/users.js @@ -9,10 +9,9 @@ exports.seed = async function seed(knex) { await knex('users').del(); await knex('users').insert({ - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: 'John', email: 'johndoe@example.com', - email_verified_at: knex.fn.now(), - password: await hash('abogoboga'), }); }; diff --git a/backend/node/src/adapters/controllers/create-user.controller.spec.ts b/backend/node/src/adapters/controllers/create-user.controller.spec.ts index cb877e3..328717f 100644 --- a/backend/node/src/adapters/controllers/create-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/create-user.controller.spec.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; -import type { HashService } from '../../core/interfaces/hash.interface'; import type { CreateUserDto, UserRepository, @@ -25,29 +24,23 @@ describe('createUserController', () => { remove: vi.fn(), truncate: vi.fn(), }; - const hashService: HashService = { - create: vi.fn(), - verify: vi.fn(), - }; const dto: CreateUserDto = { - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', email: 'johndoe@example.com', - password: 'abogoboga', - password_confirmation: 'abogoboga', }; - const controller = createUserController(userRepository, hashService); + const controller = createUserController(userRepository); let request: HttpRequestBody = {}; const user: UserTable = { id: 'id', - name: dto.name, + first_name: dto.first_name, + last_name: dto.last_name, nickname: null, email: dto.email, - email_verified_at: null, - password: dto.password, created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; @@ -56,11 +49,10 @@ describe('createUserController', () => { beforeEach(() => { mockedCreateUser.mockImplementation(() => { - const { password, ...result } = user; - return Promise.resolve({ - ...result, - name: dto.name, + ...user, + first_name: dto.first_name, + last_name: dto.last_name, email: dto.email, photo: null, avatar: null, @@ -79,11 +71,14 @@ describe('createUserController', () => { const data = await controller(request); - const { password, ...result } = user; - expect(data).toEqual>({ status: 201, - data: { ...result, photo: null, avatar: null, type: 'users' }, + data: { + ...user, + photo: null, + avatar: null, + type: 'users', + }, }); }); }); diff --git a/backend/node/src/adapters/controllers/create-user.controller.ts b/backend/node/src/adapters/controllers/create-user.controller.ts index 6348700..7186533 100644 --- a/backend/node/src/adapters/controllers/create-user.controller.ts +++ b/backend/node/src/adapters/controllers/create-user.controller.ts @@ -1,5 +1,4 @@ import { BadRequestException } from '../../core/exceptions/bad-request.exception'; -import type { HashService } from '../../core/interfaces/hash.interface'; import type { CreateUserDto, UserRepository, @@ -10,8 +9,7 @@ import type { HttpRequestBody } from '../interfaces/http.interface'; import type { UserResponse } from '../interfaces/user.interface'; export function createUserController( - userRepository: UserRepository, - hashService: HashService + userRepository: UserRepository ): ( req: HttpRequestBody> ) => Promise> { @@ -22,13 +20,19 @@ export function createUserController( throw new BadRequestException('Request body is empty.'); } - const dto = { ...req.body, photo: req.file }; + const dto = { + ...req.body, + photo: req.file, + }; - const data = await createUser(dto, userRepository, hashService); + const data = await createUser(dto, userRepository); return { status: 201, - data: { ...data, type: 'users' }, + data: { + ...data, + type: 'users', + }, }; }; } diff --git a/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts b/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts index 723ce1e..459aa96 100644 --- a/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts +++ b/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts @@ -40,11 +40,10 @@ describe('findAllUsersController', () => { let user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; @@ -53,10 +52,11 @@ describe('findAllUsersController', () => { beforeEach(() => { mockedFindAllUsers.mockImplementation(() => { - const { password, ...result } = user; - return Promise.resolve([ - { ...result, avatar: user.photo ? 'avatar.png' : null }, + { + ...user, + avatar: user.photo ? 'avatar.png' : null, + }, ]); }); }); @@ -72,12 +72,14 @@ describe('findAllUsersController', () => { test('should return all users', async () => { const data = await controller(request); - const { password, ...result } = user; - expect(data.status).toEqual(200); expect(data.data).toEqual( expect.arrayContaining([ - { ...result, avatar: null, type: 'users' }, + { + ...user, + avatar: null, + type: 'users', + }, ]) ); }); @@ -86,12 +88,15 @@ describe('findAllUsersController', () => { user = { ...user, photo: 'photo.png' }; const data = await controller(request); - const { password, ...result } = user; expect(data.status).toEqual(200); expect(data.data).toEqual( expect.arrayContaining([ - { ...result, avatar: 'avatar.png', type: 'users' }, + { + ...user, + avatar: 'avatar.png', + type: 'users', + }, ]) ); }); diff --git a/backend/node/src/adapters/controllers/find-all-users.controller.ts b/backend/node/src/adapters/controllers/find-all-users.controller.ts index ded98d6..d88bb18 100644 --- a/backend/node/src/adapters/controllers/find-all-users.controller.ts +++ b/backend/node/src/adapters/controllers/find-all-users.controller.ts @@ -14,8 +14,12 @@ export function findAllUsersController( return { status: 200, - data: - data.length > 0 ? data.map((obj) => ({ ...obj, type: 'users' })) : [], + data: data?.map((obj) => { + return { + ...obj, + type: 'users', + }; + }), }; }; } diff --git a/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts b/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts index dae2b74..719cd5d 100644 --- a/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts @@ -39,11 +39,10 @@ describe('findOneUserController', () => { let user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; @@ -52,10 +51,8 @@ describe('findOneUserController', () => { beforeEach(() => { mockedFindOneUser.mockImplementation(() => { - const { password, ...result } = user; - return Promise.resolve({ - ...result, + ...user, avatar: user.photo ? 'avatar.png' : null, }); }); @@ -71,11 +68,14 @@ describe('findOneUserController', () => { request = { ...request, params: { id: 'id' } }; const data = await controller(request); - const { password, ...result } = user; expect(data).toEqual>({ status: 200, - data: { ...result, avatar: null, type: 'users' }, + data: { + ...user, + avatar: null, + type: 'users', + }, }); }); @@ -83,11 +83,14 @@ describe('findOneUserController', () => { user = { ...user, photo: 'photo.png' }; const data = await controller(request); - const { password, ...result } = user; expect(data).toEqual>({ status: 200, - data: { ...result, avatar: 'avatar.png', type: 'users' }, + data: { + ...user, + avatar: 'avatar.png', + type: 'users', + }, }); }); }); diff --git a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts index 719da90..26888ae 100644 --- a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts +++ b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts @@ -26,11 +26,10 @@ describe('uniqueUserEmailController', () => { const user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; diff --git a/backend/node/src/adapters/controllers/update-user.controller.spec.ts b/backend/node/src/adapters/controllers/update-user.controller.spec.ts index 7aacd74..2840c6e 100644 --- a/backend/node/src/adapters/controllers/update-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/update-user.controller.spec.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; -import type { HashService } from '../../core/interfaces/hash.interface'; import type { UpdateUserDto, UserRepository, @@ -24,10 +23,6 @@ describe('updateUserController', () => { remove: vi.fn(), truncate: vi.fn(), }; - const hashService: HashService = { - create: vi.fn(), - verify: vi.fn(), - }; const fileService: FileService = { upload: vi.fn(), getUrl: vi.fn(), @@ -46,32 +41,26 @@ describe('updateUserController', () => { let user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; - const controller = updateUserController( - userRepository, - hashService, - fileService - ); + const controller = updateUserController(userRepository, fileService); const mockedUpdateUser = vi.mocked(updateUser, true); beforeEach(() => { mockedUpdateUser.mockImplementation(() => { - const { password, ...result } = user; - return Promise.resolve({ - ...result, - name: dto.name ? dto.name : result.name, + ...user, + first_name: dto.first_name ?? user.first_name, + last_name: dto.last_name ?? user.last_name, // nickname: dto.nickname ? dto.nickname : result.nickname, - email: dto.email ? dto.email : result.email, + email: dto.email ? dto.email : user.email, avatar: user.photo ? 'avatar.png' : null, }); }); @@ -87,12 +76,11 @@ describe('updateUserController', () => { request = { ...request, params: { id: 'id' } }; const data = await controller(request); - const { password, ...result } = user; expect(data).toEqual({ status: 200, data: { - ...result, + ...user, avatar: null, type: 'users', }, @@ -100,16 +88,19 @@ describe('updateUserController', () => { }); test("should only update user's name", async () => { - dto = { name: 'Jane Doe' }; + dto = { + first_name: 'John', + last_name: 'Doe', + }; const data = await controller(request); - const { password, ...result } = user; expect(data).toEqual({ status: 200, data: { - ...result, - name: 'Jane Doe', + ...user, + first_name: 'John', + last_name: 'Doe', avatar: null, type: 'users', }, @@ -119,12 +110,11 @@ describe('updateUserController', () => { test("should only update user's email", async () => { dto = { email: 'janedoe@example.com' }; const data = await controller(request); - const { password, ...result } = user; expect(data).toEqual({ status: 200, data: { - ...result, + ...user, email: 'janedoe@example.com', avatar: null, type: 'users', @@ -137,12 +127,11 @@ describe('updateUserController', () => { user = { ...user, photo: 'photo.png' }; const data = await controller(request); - const { password, ...result } = user; expect(data).toEqual({ status: 200, data: { - ...result, + ...user, avatar: 'avatar.png', type: 'users', }, diff --git a/backend/node/src/adapters/controllers/update-user.controller.ts b/backend/node/src/adapters/controllers/update-user.controller.ts index f3e2fdc..27624fc 100644 --- a/backend/node/src/adapters/controllers/update-user.controller.ts +++ b/backend/node/src/adapters/controllers/update-user.controller.ts @@ -1,6 +1,5 @@ import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; -import type { HashService } from '../../core/interfaces/hash.interface'; import type { UpdateUserDto, UserRepository, @@ -15,7 +14,6 @@ import type { export function updateUserController( userRepository: UserRepository, - hashService: HashService, fileService: FileService ): ( req: HttpRequest> @@ -31,13 +29,7 @@ export function updateUserController( const dto = { ...req.body }; - const data = await updateUser( - id, - dto, - userRepository, - hashService, - fileService - ); + const data = await updateUser(id, dto, userRepository, fileService); return { status: 200, diff --git a/backend/node/src/adapters/interfaces/http.interface.ts b/backend/node/src/adapters/interfaces/http.interface.ts index 54d4f3b..00c3542 100644 --- a/backend/node/src/adapters/interfaces/http.interface.ts +++ b/backend/node/src/adapters/interfaces/http.interface.ts @@ -33,14 +33,6 @@ export type HttpRequestCookie> = HttpRequest< T >; -export interface BearerTokenHeader { - authorization: string | undefined; -} - -export interface RefreshTokenCookie { - refresh_token: string | undefined; -} - export interface JsonApi { jsonapi: { version: string; @@ -78,8 +70,3 @@ export interface JsonApiErrorObject { export interface JsonApiError extends JsonApi { errors: JsonApiErrorObject[]; } - -export interface BearerTokenError { - error: string; - error_description: string; -} diff --git a/backend/node/src/core/entities/user.entity.ts b/backend/node/src/core/entities/user.entity.ts index acb6921..f0315d9 100644 --- a/backend/node/src/core/entities/user.entity.ts +++ b/backend/node/src/core/entities/user.entity.ts @@ -1,5 +1,6 @@ export interface User { - name: string; + first_name: string; + last_name?: string | null; nickname?: string | null; email: string; avatar?: string | null; diff --git a/backend/node/src/core/exceptions/bad-request.exception.ts b/backend/node/src/core/exceptions/bad-request.exception.ts index 446e9b7..dd50a41 100644 --- a/backend/node/src/core/exceptions/bad-request.exception.ts +++ b/backend/node/src/core/exceptions/bad-request.exception.ts @@ -7,10 +7,8 @@ export class BadRequestException extends HttpException { constructor(message: string, error?: ErrorObject) { super(400, message, 'Bad Request'); - if (error) { - if (Object.keys(error).length > 0) { - this.error = error; - } + if (error && Object.keys(error).length) { + this.error = error; } } } diff --git a/backend/node/src/core/interfaces/hash.interface.ts b/backend/node/src/core/interfaces/hash.interface.ts deleted file mode 100644 index 5840b00..0000000 --- a/backend/node/src/core/interfaces/hash.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface HashService { - create: (plain: string) => Promise; - verify: (hash: string, plain: string) => Promise; -} diff --git a/backend/node/src/core/interfaces/user.interface.ts b/backend/node/src/core/interfaces/user.interface.ts index f5f7a12..bb53923 100644 --- a/backend/node/src/core/interfaces/user.interface.ts +++ b/backend/node/src/core/interfaces/user.interface.ts @@ -1,19 +1,18 @@ import type { User } from '../entities/user.entity'; import type { BaseRepository } from './common.interface'; -export interface CreateUserDto extends Omit { +export interface CreateUserDto + extends Omit { + last_name: string | null; nickname?: string; - password: string; - password_confirmation: string; } export type UpdateUserDto = Partial; export interface UserTable extends Pick, - Omit { + Omit { id: string; - email_verified_at?: string | null; photo?: string | null; created_at: string; updated_at: string; @@ -30,4 +29,4 @@ export interface UserRepository findOneByEmail: (email: string) => Promise; } -export type UserResult = Omit & Pick; +export type UserResult = UserTable & Pick; diff --git a/backend/node/src/core/use-cases/create-user.use-case.spec.ts b/backend/node/src/core/use-cases/create-user.use-case.spec.ts index 2327b16..7e5efa9 100644 --- a/backend/node/src/core/use-cases/create-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/create-user.use-case.spec.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -import type { HashService } from '../interfaces/hash.interface'; import type { CreateUserDto, UserRepository, @@ -11,22 +10,20 @@ import { createUser } from './create-user.use-case'; describe('createUser', () => { let userRepository: UserRepository; - let hashService: HashService; const dto: CreateUserDto = { - name: 'John Doe', + first_name: 'Doe', + last_name: 'Doe', email: 'johndoe@example.com', - password: 'abogoboga', - password_confirmation: 'abogoboga', }; const user: UserTable = { id: 'id', - name: dto.name, + first_name: dto.first_name, + last_name: dto.last_name, nickname: null, email: dto.email, email_verified_at: null, - password: dto.password, created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; @@ -34,7 +31,10 @@ describe('createUser', () => { beforeEach(() => { userRepository = { create: vi.fn((data: UserTableInput) => { - return Promise.resolve({ ...user, ...data }); + return Promise.resolve({ + ...user, + ...data, + }); }), findAll: vi.fn(), findOne: vi.fn(), @@ -43,28 +43,23 @@ describe('createUser', () => { remove: vi.fn(), truncate: vi.fn(), }; - - hashService = { - create: vi.fn(), - verify: vi.fn(), - }; }); test('should throw error when user is not saved', async () => { userRepository.create = vi.fn().mockReturnValue(Promise.resolve(null)); - await expect(createUser(dto, userRepository, hashService)).rejects.toThrow( + await expect(createUser(dto, userRepository)).rejects.toThrow( new Error('Something went wrong.') ); }); test('should create an user', async () => { - const data = await createUser(dto, userRepository, hashService); - - const { password, ...result } = user; + const data = await createUser(dto, userRepository); expect(userRepository.create).toBeCalledTimes(1); - expect(hashService.create).toBeCalledTimes(1); - expect(data).toEqual({ ...result, avatar: null }); + expect(data).toEqual({ + ...user, + avatar: null, + }); }); }); diff --git a/backend/node/src/core/use-cases/create-user.use-case.ts b/backend/node/src/core/use-cases/create-user.use-case.ts index 40ec529..af8c1c1 100644 --- a/backend/node/src/core/use-cases/create-user.use-case.ts +++ b/backend/node/src/core/use-cases/create-user.use-case.ts @@ -1,4 +1,3 @@ -import type { HashService } from '../interfaces/hash.interface'; import type { CreateUserDto, UserRepository, @@ -17,25 +16,22 @@ import type { */ export async function createUser( dto: CreateUserDto, - userRepository: UserRepository, - hashService: HashService + userRepository: UserRepository ): Promise { - const { name, email, password } = dto; + const { first_name: firstName, last_name: lastName, email } = dto; const record = await userRepository.create({ - name, + first_name: firstName, + last_name: lastName, email, - password: await hashService.create(password), }); if (!record) { throw new Error('Something went wrong.'); } - const { password: ignorePassword, ...data } = record; - return { - ...data, + ...record, avatar: null, }; } diff --git a/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts b/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts index 01ef922..243b9a9 100644 --- a/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts +++ b/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts @@ -13,11 +13,10 @@ describe('findAllUsers', () => { const user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; @@ -52,12 +51,15 @@ describe('findAllUsers', () => { test('should return all users', async () => { const data = await findAllUsers(userRepository, fileService); - const { password, ...result } = user; - expect(userRepository.findAll).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(0); expect(data).toEqual( - expect.arrayContaining([{ ...result, avatar: null }]) + expect.arrayContaining([ + { + ...user, + avatar: null, + }, + ]) ); }); @@ -67,13 +69,15 @@ describe('findAllUsers', () => { .mockReturnValue(Promise.resolve([{ ...user, photo: 'photo.png' }])); const data = await findAllUsers(userRepository, fileService); - const { password, ...result } = user; - expect(userRepository.findAll).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(1); expect(data).toEqual( expect.arrayContaining([ - { ...result, photo: 'photo.png', avatar: 'avatar.png' }, + { + ...user, + photo: 'photo.png', + avatar: 'avatar.png', + }, ]) ); }); diff --git a/backend/node/src/core/use-cases/find-all-users.use-case.ts b/backend/node/src/core/use-cases/find-all-users.use-case.ts index 2cd91b0..eb4705b 100644 --- a/backend/node/src/core/use-cases/find-all-users.use-case.ts +++ b/backend/node/src/core/use-cases/find-all-users.use-case.ts @@ -15,18 +15,22 @@ export async function findAllUsers( const records = await userRepository.findAll(); const result = await Promise.all( - records.map( - async ({ password, ...obj }) => - ({ - ...obj, - avatar: obj.photo ? await fileService.getUrl(obj.photo) : null, - } as UserResult) - ) + records.map(async (obj) => { + let avatar: string | null = null; + if (obj.photo) { + avatar = await fileService.getUrl(obj.photo); + } + + return { + ...obj, + avatar, + }; + }) ); - if (records.length > 0) { - return result; + if (!records.length) { + return []; } - return []; + return result; } diff --git a/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts b/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts index c1fb839..cd92b0b 100644 --- a/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts @@ -14,11 +14,10 @@ describe('findOneUser', () => { const user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; @@ -55,25 +54,27 @@ describe('findOneUser', () => { test('should return an user without avatar', async () => { const data = await findOneUser(user.id, userRepository, fileService); - const { password, ...result } = user; - expect(userRepository.findOne).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(0); - expect(data).toEqual({ ...result, avatar: null }); + expect(data).toEqual({ + ...user, + avatar: null, + }); }); test('should return an user with avatar', async () => { - userRepository.findOne = vi - .fn() - .mockReturnValue(Promise.resolve({ ...user, photo: 'photo.png' })); + userRepository.findOne = vi.fn().mockReturnValue( + Promise.resolve({ + ...user, + photo: 'photo.png', + }) + ); const data = await findOneUser(user.id, userRepository, fileService); - const { password, ...result } = user; - expect(userRepository.findOne).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(1); expect(data).toEqual({ - ...result, + ...user, photo: 'photo.png', avatar: 'avatar.png', }); diff --git a/backend/node/src/core/use-cases/find-one-user.use-case.ts b/backend/node/src/core/use-cases/find-one-user.use-case.ts index 7ebd9a5..589a63f 100644 --- a/backend/node/src/core/use-cases/find-one-user.use-case.ts +++ b/backend/node/src/core/use-cases/find-one-user.use-case.ts @@ -23,10 +23,13 @@ export async function findOneUser( throw new NotFoundException('The user is not found.'); } - const { password, ...data } = record; + let avatar: string | null = null; + if (record.photo) { + avatar = await fileService.getUrl(record.photo); + } return { - ...data, - avatar: record.photo ? await fileService.getUrl(record.photo) : null, + ...record, + avatar, }; } diff --git a/backend/node/src/core/use-cases/remove-user.use-case.spec.ts b/backend/node/src/core/use-cases/remove-user.use-case.spec.ts index 3e17823..6eee756 100644 --- a/backend/node/src/core/use-cases/remove-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/remove-user.use-case.spec.ts @@ -10,11 +10,10 @@ describe('removeUser', () => { const user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; @@ -57,9 +56,12 @@ describe('removeUser', () => { }); test('should delete user with an avatar', async () => { - userRepository.remove = vi - .fn() - .mockReturnValue(Promise.resolve({ ...user, photo: 'photo.png' })); + userRepository.remove = vi.fn().mockReturnValue( + Promise.resolve({ + ...user, + photo: 'photo.png', + }) + ); await removeUser('id', userRepository, fileService); diff --git a/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts b/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts index d7eae60..54e2294 100644 --- a/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts +++ b/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts @@ -8,11 +8,10 @@ describe('uniqueUserEmail', () => { const user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; diff --git a/backend/node/src/core/use-cases/update-user.use-case.spec.ts b/backend/node/src/core/use-cases/update-user.use-case.spec.ts index 8eb0c33..ee33572 100644 --- a/backend/node/src/core/use-cases/update-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/update-user.use-case.spec.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; -import type { HashService } from '../interfaces/hash.interface'; import type { UpdateUserDto, UserRepository, @@ -13,16 +12,14 @@ import { updateUser } from './update-user.use-case'; describe('updateUser', () => { let userRepository: UserRepository; - let hashService: HashService; let fileService: FileService; let user: UserTable = { id: 'id', - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', nickname: null, email: 'johndoe@example.com', - email_verified_at: null, - password: 'abogoboga', photo: '', created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', @@ -47,11 +44,6 @@ describe('updateUser', () => { truncate: vi.fn(), }; - hashService = { - create: vi.fn().mockReturnValue(Promise.resolve('hashedPassword')), - verify: vi.fn(), - }; - fileService = { upload: vi.fn(), getUrl: vi.fn().mockImplementation(() => { @@ -67,86 +59,52 @@ describe('updateUser', () => { test('should throw error when user not found', async () => { await expect( - updateUser('invalid-id', dto, userRepository, hashService, fileService) + updateUser('invalid-id', dto, userRepository, fileService) ).rejects.toThrow(new NotFoundException('The user is not found.')); }); test("should only update user's name", async () => { - dto = { name: 'Jane Doe' }; - - const data = await updateUser( - 'id', - dto, - userRepository, - hashService, - fileService - ); + dto = { + first_name: 'Jane', + last_name: 'Doe', + }; - const { password, ...result } = user; + const data = await updateUser('id', dto, userRepository, fileService); - expect(hashService.create).toBeCalledTimes(0); - expect(data).toEqual({ ...result, ...dto, avatar: null }); + expect(data).toEqual({ ...user, ...dto, avatar: null }); }); test("should only update user's nickname", async () => { - dto = { nickname: 'John' }; - - const data = await updateUser( - 'id', - dto, - userRepository, - hashService, - fileService - ); + dto = { + nickname: 'John', + }; - const { password, ...result } = user; + const data = await updateUser('id', dto, userRepository, fileService); - expect(hashService.create).toBeCalledTimes(0); - expect(data).toEqual({ ...result, ...dto, avatar: null }); + expect(data).toEqual({ ...user, ...dto, avatar: null }); }); test("should only update user's email", async () => { - dto = { email: 'janedoe@example.com' }; - - const data = await updateUser( - 'id', - dto, - userRepository, - hashService, - fileService - ); - - const { password, ...result } = user; - - expect(hashService.create).toBeCalledTimes(0); - expect(data).toEqual({ ...result, ...dto, avatar: null }); - }); - - test("should only update user's password", async () => { - dto = { password: 'new-password' }; + dto = { + email: 'janedoe@example.com', + }; - await updateUser('id', dto, userRepository, hashService, fileService); + const data = await updateUser('id', dto, userRepository, fileService); - expect(hashService.create).toBeCalledTimes(1); + expect(data).toEqual({ ...user, ...dto, avatar: null }); }); test("should only update existing user's avatar", async () => { dto = {}; - user = { ...user, photo: 'photo.png' }; - - const data = await updateUser( - 'id', - dto, - userRepository, - hashService, - fileService - ); + user = { + ...user, + photo: 'photo.png', + }; - const { password, ...result } = user; + const data = await updateUser('id', dto, userRepository, fileService); - expect(hashService.create).toBeCalledTimes(0); expect(data).toEqual({ - ...result, + ...user, photo: 'photo.png', avatar: 'avatar.png', }); diff --git a/backend/node/src/core/use-cases/update-user.use-case.ts b/backend/node/src/core/use-cases/update-user.use-case.ts index 96b8f4a..b2f21b3 100644 --- a/backend/node/src/core/use-cases/update-user.use-case.ts +++ b/backend/node/src/core/use-cases/update-user.use-case.ts @@ -1,6 +1,5 @@ import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; -import type { HashService } from '../interfaces/hash.interface'; import type { UpdateUserDto, UserRepository, @@ -24,15 +23,17 @@ export async function updateUser( id: string, dto: UpdateUserDto, userRepository: UserRepository, - hashService: HashService, fileService: FileService ): Promise { - const { name, nickname, email, password } = dto; + const { first_name: firstName, nickname, email } = dto; let input: Partial = {}; - if (name) { - input = { ...input, name }; + if (firstName) { + input = { + ...input, + first_name: firstName, + }; } if (nickname) { @@ -43,20 +44,19 @@ export async function updateUser( input = { ...input, email }; } - if (password) { - input = { ...input, password: await hashService.create(password) }; - } - const record = await userRepository.update(id, input); if (!record) { throw new NotFoundException('The user is not found.'); } - const { password: ignorePassword, ...data } = record; + let avatar: string | null = null; + if (record.photo) { + avatar = await fileService.getUrl(record.photo); + } return { - ...data, - avatar: record.photo ? await fileService.getUrl(record.photo) : null, + ...record, + avatar, }; } diff --git a/backend/node/src/infrastructure/repositories/user.repository.ts b/backend/node/src/infrastructure/repositories/user.repository.ts index 97e3ef1..1a3be35 100644 --- a/backend/node/src/infrastructure/repositories/user.repository.ts +++ b/backend/node/src/infrastructure/repositories/user.repository.ts @@ -11,20 +11,20 @@ export const userRepository: UserRepository = { .insert(input) .returning([ 'id', - 'name', + 'first_name', + 'last_name', 'nickname', 'email', - 'password', 'photo', 'created_at', 'updated_at', ]); - if (records.length < 1) { + if (!records.length) { return null; } - const result = records[0] as UserTable; + const result = records.at(0) as UserTable; return result; }, @@ -32,10 +32,10 @@ export const userRepository: UserRepository = { findAll: async () => { const records = await db('users').select( 'id', - 'name', + 'first_name', + 'last_name', 'nickname', 'email', - 'password', 'photo', 'created_at', 'updated_at' @@ -51,10 +51,10 @@ export const userRepository: UserRepository = { .where('id', '=', id) .first( 'id', - 'name', + 'first_name', + 'last_name', 'nickname', 'email', - 'password', 'photo', 'created_at', 'updated_at' @@ -74,10 +74,10 @@ export const userRepository: UserRepository = { .where('email', '=', email) .first( 'id', - 'name', + 'first_name', + 'last_name', 'nickname', 'email', - 'password', 'photo', 'created_at', 'updated_at' @@ -98,20 +98,20 @@ export const userRepository: UserRepository = { .update({ ...input, updated_at: db.fn.now() }) .returning([ 'id', - 'name', + 'first_name', + 'last_name', 'nickname', 'email', - 'password', 'photo', 'created_at', 'updated_at', ]); - if (record.length < 1) { + if (!record.length) { return null; } - const result = record[0] as UserTable; + const result = record.at(0) as UserTable; return result; }, @@ -122,20 +122,20 @@ export const userRepository: UserRepository = { .delete() .returning([ 'id', - 'name', + 'first_name', + 'last_name', 'nickname', 'email', - 'password', 'photo', 'created_at', 'updated_at', ]); - if (record.length < 1) { + if (!record.length) { return null; } - const result = record[0] as UserTable; + const result = record.at(0) as UserTable; return result; }, diff --git a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts index afaece6..3ce93fa 100644 --- a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts +++ b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts @@ -20,7 +20,6 @@ import type { } from '../../../../adapters/interfaces/user.interface'; import { userRepository } from '../../../repositories/user.repository'; import { fileService } from '../../../services/file.service'; -import { hashService } from '../../../services/hash.service'; export async function create( req: Request, @@ -45,7 +44,7 @@ export async function create( }; } - const controller = createUserController(userRepository, hashService); + const controller = createUserController(userRepository); const { status, data } = await controller(request); @@ -140,11 +139,7 @@ export async function update( }; } - const controller = updateUserController( - userRepository, - hashService, - fileService - ); + const controller = updateUserController(userRepository, fileService); const { status, data } = await controller(request); diff --git a/backend/node/src/infrastructure/server/express/middlewares/error.ts b/backend/node/src/infrastructure/server/express/middlewares/error.ts index 2bfbe84..df190d9 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/error.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/error.ts @@ -2,7 +2,6 @@ import type { ErrorObject } from 'ajv'; import type { NextFunction, Request, Response } from 'express'; import { ValidationError } from 'express-json-validator-middleware'; import type { - BearerTokenError, JsonApiError, JsonApiErrorObject, } from '../../../../adapters/interfaces/http.interface'; @@ -32,10 +31,20 @@ export function errorHandler() { const { name, message } = err; let { status } = err; - let response: JsonApiError | BearerTokenError = { - jsonapi: { version: '1.0' }, - links: { self: req.path }, - errors: [{ status, title: name, detail: message }], + let response: JsonApiError = { + jsonapi: { + version: '1.0', + }, + links: { + self: req.path, + }, + errors: [ + { + status, + title: name, + detail: message, + }, + ], }; if (err instanceof ValidationError) { diff --git a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts index 1d60fa7..68ff6c7 100644 --- a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts +++ b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts @@ -3,7 +3,11 @@ import type { AllowedSchema } from 'express-json-validator-middleware'; export const createUserSchema: AllowedSchema = { type: 'object', properties: { - name: { + first_name: { + type: 'string', + minLength: 1, + }, + last_name: { type: 'string', minLength: 1, }, @@ -16,35 +20,15 @@ export const createUserSchema: AllowedSchema = { format: 'The email must be a valid email address.', }, }, - password: { - type: 'string', - minLength: 8, - errorMessage: { - minLength: 'The password must be at least 8 characters.', - }, - }, - password_confirmation: { - type: 'string', - minLength: 8, - const: { - $data: '1/password', - }, - errorMessage: { - minLength: 'The password confirmation must be at least 8 characters.', - const: 'The password confirmation does not match.', - }, - }, }, - required: ['name', 'email', 'password', 'password_confirmation'], + required: ['first_name', 'email'], errorMessage: { required: { - name: 'The name is required.', + first_name: 'The first name is required.', email: 'The email is required.', - password: 'The password is required.', - password_confirmation: 'The password confirmation is required.', }, properties: { - name: 'The name is required.', + first_name: 'The first name is required.', }, }, }; @@ -52,14 +36,18 @@ export const createUserSchema: AllowedSchema = { export const updateUserSchema: AllowedSchema = { type: 'object', properties: { - name: { + first_name: { type: 'string', minLength: 1, errorMessage: { - type: 'The name must be a string.', - minLength: 'The name field must have a value.', + type: 'The first name must be a string.', + minLength: 'The first name field must have a value.', }, }, + last_name: { + type: 'string', + nullable: true, + }, nickname: { type: 'string', nullable: true, @@ -72,37 +60,9 @@ export const updateUserSchema: AllowedSchema = { format: 'The email must be a valid email address.', }, }, - password: { - type: 'string', - minLength: 8, - errorMessage: { - type: 'The password must be a string.', - minLength: 'The password must be at least 8 characters.', - }, - }, - password_confirmation: { - type: 'string', - minLength: 8, - const: { - $data: '1/password', - }, - errorMessage: { - type: 'The password confirmation must be a string.', - minLength: 'The password confirmation must be at least 8 characters.', - const: 'The password confirmation does not match.', - }, - }, - }, - dependencies: { - password: ['password_confirmation'], - password_confirmation: ['password'], }, + dependencies: {}, errorMessage: { - dependencies: { - password: - 'The password confirmation field is required when password is present.', - password_confirmation: - 'The password field is required when password confirmation is present.', - }, + dependencies: {}, }, }; diff --git a/backend/node/src/infrastructure/services/hash.service.ts b/backend/node/src/infrastructure/services/hash.service.ts deleted file mode 100644 index 3cea56e..0000000 --- a/backend/node/src/infrastructure/services/hash.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { hash, verify } from 'argon2'; -import type { HashService } from '../../core/interfaces/hash.interface'; - -export const hashService: HashService = { - create: async (plain: string) => { - const hashed = await hash(plain); - - return hashed; - }, - - verify: async (hashed: string, plain: string) => { - const isValid = await verify(hashed, plain); - - return isValid; - }, -}; diff --git a/backend/node/tsconfig.tsnode.json b/backend/node/tsconfig.tsnode.json index d53ed09..edaff4e 100644 --- a/backend/node/tsconfig.tsnode.json +++ b/backend/node/tsconfig.tsnode.json @@ -1,106 +1,98 @@ { - "compilerOptions": { - "strict": true, - "allowUnusedLabels": false, - "allowUnreachableCode": false, - "exactOptionalPropertyTypes": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "isolatedModules": true, - "checkJs": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "lib": [ - "es2023" - ], - "module": "node16", - "target": "es2022", - "moduleResolution": "node16", - "baseUrl": "./", - "outDir": "./dist", - "incremental": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "removeComments": true, - "typeRoots": [ - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/types", - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/node_modules/@types", - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/types", - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/node_modules/@types", - "/home/husen/Projects/playground/backend/node/node_modules/@types" - ] - }, - "files": [ - "./src/adapters/controllers/create-user.controller.ts", - "./src/adapters/controllers/find-all-users.controller.ts", - "./src/adapters/controllers/find-one-user.controller.ts", - "./src/adapters/controllers/home.controller.ts", - "./src/adapters/controllers/remove-user.controller.ts", - "./src/adapters/controllers/unique-user-email.controller.ts", - "./src/adapters/controllers/update-user.controller.ts", - "./src/adapters/controllers/validate-uuid.controller.ts", - "./src/adapters/interfaces/common.interface.ts", - "./src/adapters/interfaces/http.interface.ts", - "./src/adapters/interfaces/user.interface.ts", - "./src/core/entities/common.entity.ts", - "./src/core/entities/user.entity.ts", - "./src/core/exceptions/bad-request.exception.ts", - "./src/core/exceptions/http.exception.ts", - "./src/core/exceptions/not-found.exception.ts", - "./src/core/interfaces/common.interface.ts", - "./src/core/interfaces/file.interface.ts", - "./src/core/interfaces/hash.interface.ts", - "./src/core/interfaces/http.interface.ts", - "./src/core/interfaces/redis.interface.ts", - "./src/core/interfaces/user.interface.ts", - "./src/core/use-cases/create-user.use-case.ts", - "./src/core/use-cases/find-all-users.use-case.ts", - "./src/core/use-cases/find-one-user.use-case.ts", - "./src/core/use-cases/remove-user.use-case.ts", - "./src/core/use-cases/unique-user-email.use-case.ts", - "./src/core/use-cases/update-user.use-case.ts", - "./src/core/use-cases/validate-uuid.use-case.ts", - "./src/infrastructure/config/app.ts", - "./src/infrastructure/config/auth.ts", - "./src/infrastructure/config/database.ts", - "./src/infrastructure/config/redis.ts", - "./src/infrastructure/config/s3.ts", - "./src/infrastructure/ports/database.ts", - "./src/infrastructure/ports/logger.ts", - "./src/infrastructure/ports/redis.ts", - "./src/infrastructure/ports/s3.ts", - "./src/infrastructure/repositories/user.repository.ts", - "./src/infrastructure/server/express/app.ts", - "./src/infrastructure/server/express/index.ts", - "./src/infrastructure/server/express/handlers/app.handler.ts", - "./src/infrastructure/server/express/handlers/user.handler.ts", - "./src/infrastructure/server/express/middlewares/error.ts", - "./src/infrastructure/server/express/middlewares/logger.ts", - "./src/infrastructure/server/express/middlewares/unique-user-email.ts", - "./src/infrastructure/server/express/middlewares/validate-uuid.ts", - "./src/infrastructure/server/express/middlewares/validator.ts", - "./src/infrastructure/server/express/routes/app.route.ts", - "./src/infrastructure/server/express/routes/index.ts", - "./src/infrastructure/server/express/routes/user.route.ts", - "./src/infrastructure/server/express/schemas/auth.schema.ts", - "./src/infrastructure/server/express/schemas/common.schema.ts", - "./src/infrastructure/server/express/schemas/user.schema.ts", - "./src/infrastructure/server/express/types/express/index.d.ts", - "./src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts", - "./src/infrastructure/services/file.service.ts", - "./src/infrastructure/services/hash.service.ts", - "./src/infrastructure/services/redis.service.ts" - ], - "include": [ - "./src/**/*.ts" - ], - "exclude": [ - "./src/**/*.spec.ts" - ] + "compilerOptions": { + "strict": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "isolatedModules": true, + "checkJs": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "moduleResolution": "node16", + "baseUrl": "./", + "outDir": "./dist", + "incremental": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "removeComments": true, + "typeRoots": [ + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/types", + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/node_modules/@types", + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/types", + "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/node_modules/@types", + "/home/husen/Projects/playground/backend/node/node_modules/@types" + ] + }, + "files": [ + "./src/adapters/controllers/create-user.controller.ts", + "./src/adapters/controllers/find-all-users.controller.ts", + "./src/adapters/controllers/find-one-user.controller.ts", + "./src/adapters/controllers/home.controller.ts", + "./src/adapters/controllers/remove-user.controller.ts", + "./src/adapters/controllers/unique-user-email.controller.ts", + "./src/adapters/controllers/update-user.controller.ts", + "./src/adapters/controllers/validate-uuid.controller.ts", + "./src/adapters/interfaces/common.interface.ts", + "./src/adapters/interfaces/http.interface.ts", + "./src/adapters/interfaces/user.interface.ts", + "./src/core/entities/common.entity.ts", + "./src/core/entities/user.entity.ts", + "./src/core/exceptions/bad-request.exception.ts", + "./src/core/exceptions/http.exception.ts", + "./src/core/exceptions/not-found.exception.ts", + "./src/core/interfaces/common.interface.ts", + "./src/core/interfaces/file.interface.ts", + "./src/core/interfaces/http.interface.ts", + "./src/core/interfaces/redis.interface.ts", + "./src/core/interfaces/user.interface.ts", + "./src/core/use-cases/create-user.use-case.ts", + "./src/core/use-cases/find-all-users.use-case.ts", + "./src/core/use-cases/find-one-user.use-case.ts", + "./src/core/use-cases/remove-user.use-case.ts", + "./src/core/use-cases/unique-user-email.use-case.ts", + "./src/core/use-cases/update-user.use-case.ts", + "./src/core/use-cases/validate-uuid.use-case.ts", + "./src/infrastructure/config/app.ts", + "./src/infrastructure/config/auth.ts", + "./src/infrastructure/config/database.ts", + "./src/infrastructure/config/redis.ts", + "./src/infrastructure/config/s3.ts", + "./src/infrastructure/ports/database.ts", + "./src/infrastructure/ports/logger.ts", + "./src/infrastructure/ports/redis.ts", + "./src/infrastructure/ports/s3.ts", + "./src/infrastructure/repositories/user.repository.ts", + "./src/infrastructure/server/express/app.ts", + "./src/infrastructure/server/express/index.ts", + "./src/infrastructure/server/express/handlers/app.handler.ts", + "./src/infrastructure/server/express/handlers/user.handler.ts", + "./src/infrastructure/server/express/middlewares/error.ts", + "./src/infrastructure/server/express/middlewares/logger.ts", + "./src/infrastructure/server/express/middlewares/unique-user-email.ts", + "./src/infrastructure/server/express/middlewares/validate-uuid.ts", + "./src/infrastructure/server/express/middlewares/validator.ts", + "./src/infrastructure/server/express/routes/app.route.ts", + "./src/infrastructure/server/express/routes/index.ts", + "./src/infrastructure/server/express/routes/user.route.ts", + "./src/infrastructure/server/express/schemas/auth.schema.ts", + "./src/infrastructure/server/express/schemas/common.schema.ts", + "./src/infrastructure/server/express/schemas/user.schema.ts", + "./src/infrastructure/server/express/types/express/index.d.ts", + "./src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts", + "./src/infrastructure/services/file.service.ts", + "./src/infrastructure/services/redis.service.ts" + ], + "include": ["./src/**/*.ts"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/docker-compose-gateway.yml b/docker-compose-ory.yml similarity index 100% rename from docker-compose-gateway.yml rename to docker-compose-ory.yml diff --git a/docker-compose.yml b/docker-compose.yml index 1b81758..9527bdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: postgres: - image: docker.io/library/postgres:14-alpine + image: docker.io/library/postgres:16-alpine ports: - '${DB_PORT:-5432}:5432' volumes: @@ -28,7 +28,7 @@ services: - playground redis: - image: docker.io/library/redis:6-alpine + image: docker.io/library/redis:7-alpine ports: - '${REDIS_PORT:-6379}:6379' volumes: diff --git a/openapi.yaml b/openapi.yaml index 5a377a4..c54032a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -299,18 +299,14 @@ components: User: type: object properties: - name: + first_name: + type: string + last_name: type: string nickname: type: string email: type: string - password: - type: string - writeOnly: true - password_confirmation: - type: string - writeOnly: true photo: type: string writeOnly: true @@ -324,16 +320,13 @@ components: type: string readOnly: true required: - - "name" + - "first_name" - "email" - - "password" - - "password_confirmation" example: - name: John Doe + first_name: John + last_name: Doe nickname: John email: johndoe@example.com - password: abogoboga - password_confirmation: abogoboga photo: avatar: UserData: @@ -353,7 +346,8 @@ components: id: type: users attributes: - name: John Doe + first_name: John + last_name: Doe nickname: John email: johndoe@example.com avatar: @@ -372,7 +366,8 @@ components: - id: type: users attributes: - name: John Doe + first_name: John + last_name: Doe nickname: John email: johndoe@example.com avatar: @@ -389,11 +384,10 @@ components: id: type: users attributes: - name: John Doe + first_name: John + last_name: Doe nickname: John - email: johndoe@example.com - password: abogoboga - password_confirmation: abogoboga + email: johndoe@example. photo: UserError: allOf: @@ -410,21 +404,6 @@ components: detail: The email has already been taken. source: pointer: "/data/attributes/email" - - status: 400 - title: Bad Request - detail: The password must be at least 8 characters. - source: - pointer: "/data/attributes/password" - - status: 400 - title: Bad Request - detail: The password_confirmation must be at least 8 characters. - source: - pointer: "/data/attributes/password_confirmation" - - status: 400 - title: Bad Request - detail: The password_confirmation and password must match. - source: - pointer: "/data/attributes/password_confirmation" UserRequestError: allOf: - $ref: "#/components/schemas/UserError" @@ -432,24 +411,14 @@ components: errors: - status: 400 title: Bad Request - detail: The name is required. + detail: The first name is required. source: - pointer: "/data/attributes/name" + pointer: "/data/attributes/first_name" - status: 400 title: Bad Request detail: The email is required. source: pointer: "/data/attributes/email" - - status: 400 - title: Bad Request - detail: The password is required. - source: - pointer: "/data/attributes/password" - - status: 400 - title: Bad Request - detail: The password_confirmation is required. - source: - pointer: "/data/attributes/password_confirmation" UserNotFound: allOf: - $ref: "#/components/schemas/Error" diff --git a/shell.nix b/shell.nix index b45e2ce..28d9959 100644 --- a/shell.nix +++ b/shell.nix @@ -2,7 +2,11 @@ pkgs.mkShell { buildInputs = [ pkgs.go + pkgs.gopls + pkgs.gotools + pkgs.delve pkgs.nodejs pkgs.nodePackages.pnpm + pkgs.nodePackages.typescript-language-server ]; } From 1bd3c4174a5637d29d679942a2a48cded7e8c222 Mon Sep 17 00:00:00 2001 From: Husen Date: Tue, 30 Jan 2024 22:20:15 +0700 Subject: [PATCH 3/8] chore: migrate ts-node to swc Signed-off-by: Husen --- .github/workflows/node.yml | 9 +- .vscode/settings.json | 2 +- backend/node/.dockerignore | 37 + backend/node/.env.example | 2 +- backend/node/.env.testing | 23 + backend/node/.swcrc.old | 10 + backend/node/__tests__/app.test.ts | 17 +- backend/node/__tests__/mocks/user.mock.ts | 24 + backend/node/__tests__/user.test.ts | 589 ++-- backend/node/database/seeds/users.js | 1 - .../node/{jest.config.ts => jest.config.js} | 12 +- backend/node/package.json | 46 +- backend/node/pnpm-lock.yaml | 3080 +++++++++++------ backend/node/pnpm-workspace.yaml | 2 - .../create-user.controller.spec.ts | 33 +- .../controllers/create-user.controller.ts | 14 +- .../find-all-users.controller.spec.ts | 46 +- .../controllers/find-all-users.controller.ts | 20 +- .../find-one-user.controller.spec.ts | 55 +- .../controllers/find-one-user.controller.ts | 19 +- .../controllers/home.controller.spec.ts | 6 +- .../adapters/controllers/home.controller.ts | 3 +- .../remove-user.controller.spec.ts | 40 +- .../controllers/remove-user.controller.ts | 5 +- .../unique-user-email.controller.spec.ts | 57 +- .../unique-user-email.controller.ts | 31 +- .../update-user.controller.spec.ts | 40 +- .../controllers/update-user.controller.ts | 15 +- .../validate-uuid.controller.spec.ts | 12 +- .../src/adapters/interfaces/http.interface.ts | 6 +- .../src/adapters/interfaces/user.interface.ts | 2 +- .../core/exceptions/bad-request.exception.ts | 3 +- .../core/exceptions/not-found.exception.ts | 3 +- .../src/core/interfaces/common.interface.ts | 2 +- .../src/core/interfaces/file.interface.ts | 2 +- .../src/core/interfaces/user.interface.ts | 13 +- .../use-cases/create-user.use-case.spec.ts | 20 +- .../use-cases/find-all-users.use-case.spec.ts | 35 +- .../use-cases/find-one-user.use-case.spec.ts | 27 +- .../use-cases/remove-user.use-case.spec.ts | 23 +- .../unique-user-email.use-case.spec.ts | 19 +- .../use-cases/unique-user-email.use-case.ts | 5 +- .../use-cases/update-user.use-case.spec.ts | 43 +- .../use-cases/validate-uuid.use-case.spec.ts | 1 - backend/node/src/infrastructure/config/app.ts | 2 + .../repositories/user.repository.ts | 12 +- .../src/infrastructure/server/express/app.ts | 6 +- .../server/express/handlers/user.handler.ts | 124 +- .../server/express/middlewares/error.ts | 7 +- .../server/express/middlewares/logger.ts | 9 +- .../express/middlewares/unique-user-email.ts | 25 +- .../express/middlewares/validate-uuid.ts | 4 +- .../server/express/package.json | 23 - .../server/express/pnpm-lock.yaml | 781 ----- .../server/express/routes/index.ts | 2 +- .../server/express/schemas/user.schema.ts | 213 +- .../infrastructure/services/file.service.ts | 6 +- backend/node/tsconfig.base.json | 11 +- backend/node/tsconfig.build.json | 4 + backend/node/tsconfig.eslint.json | 2 +- backend/node/tsconfig.json | 2 +- backend/node/tsconfig.tsnode.json | 98 - backend/node/vitest.config.ts | 12 - .../hooks/after-registration.jsonnet | 5 + config/kratos/email-password/kratos.yml | 8 + config/oathkeeper/access-rules.yml | 23 + docker-compose-express.yml | 46 + docker-compose-ory.yml | 33 +- docker-compose-sveltekit.yml | 0 express.Dockerfile | 10 + openapi.yaml | 4 +- shell.nix | 10 +- templates.sublime-project | 18 - 73 files changed, 3129 insertions(+), 2825 deletions(-) create mode 100644 backend/node/.dockerignore create mode 100644 backend/node/.env.testing create mode 100644 backend/node/.swcrc.old create mode 100644 backend/node/__tests__/mocks/user.mock.ts rename backend/node/{jest.config.ts => jest.config.js} (53%) delete mode 100644 backend/node/pnpm-workspace.yaml delete mode 100644 backend/node/src/infrastructure/server/express/package.json delete mode 100644 backend/node/src/infrastructure/server/express/pnpm-lock.yaml create mode 100644 backend/node/tsconfig.build.json delete mode 100644 backend/node/tsconfig.tsnode.json delete mode 100644 backend/node/vitest.config.ts create mode 100644 config/kratos/email-password/hooks/after-registration.jsonnet delete mode 100644 docker-compose-sveltekit.yml create mode 100644 express.Dockerfile delete mode 100644 templates.sublime-project diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index dc57983..677fdb6 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ['18', '20', '21'] + node: ['20', '21'] env: FOLDER: backend/node @@ -46,7 +46,7 @@ jobs: - name: Install dependencies run: | cd ${{ env.FOLDER }} - pnpm i + pnpm install - name: Run linter run: | @@ -58,6 +58,11 @@ jobs: cd ${{ env.FOLDER }} pnpm prettier + - name: Run typecheck + run: | + cd ${{ env.FOLDER }} + pnpm typecheck + - name: Run build run: | cd ${{ env.FOLDER }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 2e1e861..c85594f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "eslint.workingDirectories": [ - "./node" + "./backend/node" ] } diff --git a/backend/node/.dockerignore b/backend/node/.dockerignore new file mode 100644 index 0000000..cbcec5f --- /dev/null +++ b/backend/node/.dockerignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# Dependency directories +node_modules + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# dotenv environment variable files +.env + +# build +dist + +coverage diff --git a/backend/node/.env.example b/backend/node/.env.example index d051289..e17c7d8 100644 --- a/backend/node/.env.example +++ b/backend/node/.env.example @@ -1,4 +1,4 @@ -APP_NAME=Randedid +APP_NAME=Playground APP_PORT=3000 DB_HOST=127.0.0.1 diff --git a/backend/node/.env.testing b/backend/node/.env.testing new file mode 100644 index 0000000..bc5ec69 --- /dev/null +++ b/backend/node/.env.testing @@ -0,0 +1,23 @@ +APP_NAME=Playground +APP_PORT=3000 +APP_URL=http://localhost:3000 + +DB_HOST=localhost +DB_PORT=5432 +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_DATABASE=postgres + +AWS_ENDPOINT=127.0.0.1 +AWS_PORT=9000 +AWS_DEFAULT_REGION=ap-southeast-1 +AWS_ACCESS_KEY_ID=miniosudo +AWS_SECRET_ACCESS_KEY=miniosudo + +REDIS_HOST=redis +REDIS_PORT=6379 + +JWT_ACCESS_SECRET=jwtaccesssecret +JWT_REFRESH_SECRET=jwtrefreshsecret +JWT_ISSUER=jwtaccessissuer +JWT_AUDIENCE=jwtaccessaudience diff --git a/backend/node/.swcrc.old b/backend/node/.swcrc.old new file mode 100644 index 0000000..8fbeb8b --- /dev/null +++ b/backend/node/.swcrc.old @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "transform": { + "hidden": { + "jest": true + } + } + } +} diff --git a/backend/node/__tests__/app.test.ts b/backend/node/__tests__/app.test.ts index 7640363..a194ba8 100644 --- a/backend/node/__tests__/app.test.ts +++ b/backend/node/__tests__/app.test.ts @@ -1,5 +1,4 @@ -import { afterAll, describe, expect, test } from 'vitest'; -import type { JsonApiError } from '../src/adapters/interfaces/http.interface'; +import { StatusCodes } from 'http-status-codes'; import { request } from './setup'; import { close } from './teardown'; @@ -13,8 +12,8 @@ describe('GET /', () => { .get('/') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(200); - expect(response.body).toEqual('Hello world!'); + expect(response.status).toEqual(StatusCodes.OK); + expect(response.body).toEqual('Hello world!'); }); }); @@ -24,15 +23,17 @@ describe('GET /404', () => { .get('/404') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ - jsonapi: { version: '1.0' }, + expect(response.status).toEqual(StatusCodes.NOT_FOUND); + expect(response.body).toEqual({ + jsonapi: { + version: '1.1', + }, links: { self: '/404', }, errors: [ { - status: 404, + status: StatusCodes.NOT_FOUND, title: 'Not Found', detail: 'Resource not found.', }, diff --git a/backend/node/__tests__/mocks/user.mock.ts b/backend/node/__tests__/mocks/user.mock.ts new file mode 100644 index 0000000..9119903 --- /dev/null +++ b/backend/node/__tests__/mocks/user.mock.ts @@ -0,0 +1,24 @@ +import { createUser } from '../../src/core/use-cases/create-user.use-case'; +import { findAllUsers } from '../../src/core/use-cases/find-all-users.use-case'; +import { findOneUser } from '../../src/core/use-cases/find-one-user.use-case'; +import { removeUser } from '../../src/core/use-cases/remove-user.use-case'; +import { uniqueUserEmail } from '../../src/core/use-cases/unique-user-email.use-case'; +import { updateUser } from '../../src/core/use-cases/update-user.use-case'; + +jest.mock('../../src/core/use-cases/update-user.use-case'); +export const mockedUpdateUser = jest.mocked(updateUser); + +jest.mock('../../src/core/use-cases/find-one-user.use-case'); +export const mockedFindOneUser = jest.mocked(findOneUser); + +jest.mock('../../src/core/use-cases/remove-user.use-case'); +export const mockedRemoveUser = jest.mocked(removeUser); + +jest.mock('../../src/core/use-cases/create-user.use-case'); +export const mockedCreateUser = jest.mocked(createUser); + +jest.mock('../../src/core/use-cases/unique-user-email.use-case'); +export const mockedUniqueUserEmail = jest.mocked(uniqueUserEmail); + +jest.mock('../../src/core/use-cases/find-all-users.use-case'); +export const mockedFindAllUsers = jest.mocked(findAllUsers); diff --git a/backend/node/__tests__/user.test.ts b/backend/node/__tests__/user.test.ts index daae262..b009024 100644 --- a/backend/node/__tests__/user.test.ts +++ b/backend/node/__tests__/user.test.ts @@ -1,8 +1,4 @@ -import { afterAll, beforeEach, describe, expect, test } from 'vitest'; -import type { - JsonApi, - JsonApiError, -} from '../src/adapters/interfaces/http.interface'; +import { ReasonPhrases, StatusCodes } from 'http-status-codes'; import { setup, testUser, user } from './fixtures/user.fixture'; import { request } from './setup'; import { close } from './teardown'; @@ -15,30 +11,25 @@ afterAll(async () => { await close(); }); -describe('POST /user', () => { +describe('POST /users', () => { test('should return error when request body empty', async () => { const response = await request - .post('/user') + .post('/users') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: '/user', + self: '/users', }, errors: [ { - status: 400, - title: 'Bad Request', - detail: 'The first name is required.', - }, - { - status: 400, - title: 'Bad Request', - detail: 'The email is required.', + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, + detail: 'The data property is required.', }, ], }); @@ -46,27 +37,33 @@ describe('POST /user', () => { test('should return error when email is invalid', async () => { const response = await request - .post('/user') + .post('/users') .set('Accept', 'application/vnd.api+json') .send({ - first_name: user.first_name, - last_name: user.last_name, - email: 'johndoe', + data: { + type: 'users', + attributes: { + first_name: user.first_name, + last_name: user.last_name, + email: 'johndoe', + }, + }, }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: '/user', + self: '/users', }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The email must be a valid email address.', + detail: + 'The data.attributes.email property must be a valid email address.', }, ], }); @@ -74,25 +71,30 @@ describe('POST /user', () => { test('should return error when email is already taken', async () => { const response = await request - .post('/user') + .post('/users') .set('Accept', 'application/vnd.api+json') .send({ - first_name: user.first_name, - last_name: user.last_name, - email: user.email, + data: { + type: 'users', + attributes: { + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + }, + }, }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: '/user', + self: '/users', }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', detail: 'The email has already been taken.', }, @@ -102,123 +104,89 @@ describe('POST /user', () => { test('should create an user', async () => { const response = await request - .post('/user') + .post('/users') .set('Accept', 'application/vnd.api+json') .send({ - first_name: testUser.first_name, - last_name: testUser.last_name, - email: testUser.email, + data: { + type: 'users', + attributes: { + first_name: testUser.first_name, + last_name: testUser.last_name, + email: testUser.email, + }, + }, }); - expect(response.status).toEqual(201); - expect(response.body).toEqual( - expect.objectContaining({ - jsonapi: { - version: '1.0', - }, - links: { - self: '/user', - }, - }) - ); - expect(response.body).toHaveProperty('data.id'); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toEqual(StatusCodes.CREATED); + expect(response.body).toHaveProperty('jsonapi.version', '1.1'); + expect(response.body).toHaveProperty('data.id'); + expect(response.body).toHaveProperty('data.type', 'users'); + expect(response.body).toHaveProperty( 'data.attributes.first_name', testUser.first_name ); - expect(response.body).toHaveProperty( + expect(response.body).toHaveProperty( 'data.attributes.last_name', testUser.last_name ); - expect(response.body).toHaveProperty('data.attributes.nickname'); - expect(response.body).toHaveProperty( + expect(response.body).toHaveProperty('data.attributes.nickname'); + expect(response.body).toHaveProperty( 'data.attributes.email', testUser.email ); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + expect(response.body).toHaveProperty('data.attributes.photo', null); + expect(response.body).toHaveProperty('data.attributes.avatar', null); + expect(response.body).toHaveProperty('data.attributes.created_at'); + expect(response.body).toHaveProperty('data.attributes.updated_at'); }); }); -describe('GET /user', () => { +describe('GET /users', () => { test('should return users', async () => { const response = await request - .get('/user') + .get('/users') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(200); - expect(response.body).toEqual( - expect.objectContaining({ - jsonapi: { - version: '1.0', - }, - links: { - self: '/user', - }, - }) - ); - expect(response.body).toHaveProperty(['data', 0, 'type'], 'users'); - expect(response.body).toHaveProperty( - ['data', 0, 'attributes', 'first_name'], + expect(response.status).toEqual(StatusCodes.OK); + expect(response.body).toHaveProperty('jsonapi.version', '1.1'); + expect(response.body).toHaveProperty('data[0].type', 'users'); + expect(response.body).toHaveProperty( + 'data[0].attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( - ['data', 0, 'attributes', 'last_name'], + expect(response.body).toHaveProperty( + 'data[0].attributes.last_name', user.last_name ); - expect(response.body).toHaveProperty([ - 'data', - 0, - 'attributes', - 'nickname', - ]); - expect(response.body).toHaveProperty( - ['data', 0, 'attributes', 'email'], + expect(response.body).toHaveProperty('data[0].attributes.nickname'); + expect(response.body).toHaveProperty( + 'data[0].attributes.email', user.email ); - expect(response.body).toHaveProperty( - ['data', 0, 'attributes', 'photo'], - null - ); - expect(response.body).toHaveProperty( - ['data', 0, 'attributes', 'avatar'], - null - ); - expect(response.body).toHaveProperty([ - 'data', - 0, - 'attributes', - 'created_at', - ]); - expect(response.body).toHaveProperty([ - 'data', - 0, - 'attributes', - 'updated_at', - ]); + expect(response.body).toHaveProperty('data[0].attributes.photo', null); + expect(response.body).toHaveProperty('data[0].attributes.avatar', null); + expect(response.body).toHaveProperty('data[0].attributes.created_at'); + expect(response.body).toHaveProperty('data[0].attributes.updated_at'); }); }); -describe('GET /user/{id}', () => { +describe('GET /users/{id}', () => { test('should return error when parameter is invalid', async () => { const response = await request - .get('/user/invalid-id') + .get('/users/invalid-id') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: '/user/invalid-id', + self: '/users/invalid-id', }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', detail: 'Validation failed (uuid v4 is expected).', }, @@ -228,20 +196,20 @@ describe('GET /user/{id}', () => { test('should return error when user is not found', async () => { const response = await request - .get('/user/60677a98-a65e-4abc-831c-45dd76e8f990') + .get('/users/60677a98-a65e-4abc-831c-45dd76e8f990') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.NOT_FOUND); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: '/user/60677a98-a65e-4abc-831c-45dd76e8f990', + self: '/users/60677a98-a65e-4abc-831c-45dd76e8f990', }, errors: [ { - status: 404, + status: StatusCodes.NOT_FOUND, title: 'Not Found', detail: 'The user is not found.', }, @@ -251,59 +219,47 @@ describe('GET /user/{id}', () => { test('should return an user', async () => { const response = await request - .get(`/user/${user.id}`) + .get(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(200); - expect(response.body).toEqual( - expect.objectContaining({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, - }, - }) - ); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toEqual(StatusCodes.OK); + expect(response.body).toHaveProperty('jsonapi.version', '1.1'); + expect(response.body).toHaveProperty('data.id', user.id); + expect(response.body).toHaveProperty('data.type', 'users'); + expect(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect(response.body).toHaveProperty( 'data.attributes.last_name', user.last_name ); - expect(response.body).toHaveProperty('data.attributes.nickname'); - expect(response.body).toHaveProperty( - 'data.attributes.email', - user.email - ); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + expect(response.body).toHaveProperty('data.attributes.nickname'); + expect(response.body).toHaveProperty('data.attributes.email', user.email); + expect(response.body).toHaveProperty('data.attributes.photo', null); + expect(response.body).toHaveProperty('data.attributes.avatar', null); + expect(response.body).toHaveProperty('data.attributes.created_at'); + expect(response.body).toHaveProperty('data.attributes.updated_at'); }); }); -describe('PATCH /user/{id}', () => { +describe('PATCH /users/{id}', () => { test('should return error when parameter is invalid', async () => { const response = await request - .patch('/user/invalid-id') + .patch('/users/invalid-id') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: '/user/invalid-id', + self: '/users/invalid-id', }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', detail: 'Validation failed (uuid v4 is expected).', }, @@ -313,21 +269,26 @@ describe('PATCH /user/{id}', () => { test('should return error when user is not found', async () => { const response = await request - .patch('/user/60677a98-a65e-4abc-831c-45dd76e8f990') - .set('Accept', 'application/vnd.api+json'); + .patch('/users/60677a98-a65e-4abc-831c-45dd76e8f990') + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + type: 'users', + }, + }); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.NOT_FOUND); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: '/user/60677a98-a65e-4abc-831c-45dd76e8f990', + self: '/users/60677a98-a65e-4abc-831c-45dd76e8f990', }, errors: [ { - status: 404, - title: 'Not Found', + status: StatusCodes.NOT_FOUND, + title: ReasonPhrases.NOT_FOUND, detail: 'The user is not found.', }, ], @@ -336,23 +297,30 @@ describe('PATCH /user/{id}', () => { test('should return error when name is null', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ first_name: null }); + .send({ + data: { + type: 'users', + attributes: { + first_name: null, + }, + }, + }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: `/user/${user.id}`, + self: `/users/${user.id}`, }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The first name must be a string.', + detail: 'The data.attributes.first_name must be a string.', }, ], }); @@ -360,23 +328,30 @@ describe('PATCH /user/{id}', () => { test('should return error when name is empty', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ first_name: '' }); + .send({ + data: { + type: 'users', + attributes: { + first_name: '', + }, + }, + }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: `/user/${user.id}`, + self: `/users/${user.id}`, }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The first name field must have a value.', + detail: 'The data.attributes.first_name field must have a value.', }, ], }); @@ -384,25 +359,30 @@ describe('PATCH /user/{id}', () => { test('should return error when name is invalid', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ - first_name: 12345, + data: { + type: 'users', + attributes: { + first_name: 12345, + }, + }, }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: `/user/${user.id}`, + self: `/users/${user.id}`, }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The first name must be a string.', + detail: 'The data.attributes.first_name must be a string.', }, ], }); @@ -410,105 +390,96 @@ describe('PATCH /user/{id}', () => { test('should ok when username is null', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ nickname: null }); - - expect(response.status).toEqual(200); - expect(response.body).toEqual( - expect.objectContaining({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, + .send({ + data: { + type: 'users', + attributes: { + nickname: null, + }, }, - }) - ); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + }); + + expect(response.status).toEqual(StatusCodes.OK); + expect(response.body).toHaveProperty('jsonapi.version', '1.1'); + expect(response.body).toHaveProperty('data.id', user.id); + expect(response.body).toHaveProperty('data.type', 'users'); + expect(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect(response.body).toHaveProperty( 'data.attributes.last_name', user.last_name ); - expect(response.body).toHaveProperty( - 'data.attributes.nickname', - null - ); - expect(response.body).toHaveProperty( - 'data.attributes.email', - user.email - ); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + expect(response.body).toHaveProperty('data.attributes.nickname', null); + expect(response.body).toHaveProperty('data.attributes.email', user.email); + expect(response.body).toHaveProperty('data.attributes.photo', null); + expect(response.body).toHaveProperty('data.attributes.avatar', null); + expect(response.body).toHaveProperty('data.attributes.created_at'); + expect(response.body).toHaveProperty('data.attributes.updated_at'); }); test('should ok when username is empty', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ nickname: '' }); - - expect(response.status).toEqual(200); - expect(response.body).toEqual( - expect.objectContaining({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, + .send({ + data: { + type: 'users', + attributes: { + nickname: '', + }, }, - }) - ); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + }); + + expect(response.status).toEqual(StatusCodes.OK); + expect(response.body).toHaveProperty('jsonapi.version', '1.1'); + expect(response.body).toHaveProperty('data.id', user.id); + expect(response.body).toHaveProperty('data.type', 'users'); + expect(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect(response.body).toHaveProperty( 'data.attributes.last_name', user.last_name ); - expect(response.body).toHaveProperty( - 'data.attributes.nickname', - null - ); - expect(response.body).toHaveProperty( - 'data.attributes.email', - user.email - ); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + expect(response.body).toHaveProperty('data.attributes.nickname', null); + expect(response.body).toHaveProperty('data.attributes.email', user.email); + expect(response.body).toHaveProperty('data.attributes.photo', null); + expect(response.body).toHaveProperty('data.attributes.avatar', null); + expect(response.body).toHaveProperty('data.attributes.created_at'); + expect(response.body).toHaveProperty('data.attributes.updated_at'); }); test('should return error when email is null', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ email: null }); + .send({ + data: { + type: 'users', + attributes: { + email: null, + }, + }, + }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: `/user/${user.id}`, + self: `/users/${user.id}`, }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The email must be a string.', + detail: 'The data.attributes.email must be a string.', }, ], }); @@ -516,23 +487,30 @@ describe('PATCH /user/{id}', () => { test('should return error when email is empty', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ email: '' }); + .send({ + data: { + type: 'users', + attributes: { + email: '', + }, + }, + }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: `/user/${user.id}`, + self: `/users/${user.id}`, }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The email must be a valid email address.', + detail: 'The data.attributes.email must be a valid email address.', }, ], }); @@ -540,23 +518,30 @@ describe('PATCH /user/{id}', () => { test('should return error when email is invalid', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ email: 12345 }); + .send({ + data: { + type: 'users', + attributes: { + email: 12345, + }, + }, + }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: `/user/${user.id}`, + self: `/users/${user.id}`, }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The email must be a string.', + detail: 'The data.attributes.email must be a string.', }, ], }); @@ -564,23 +549,30 @@ describe('PATCH /user/{id}', () => { test('should return error when email is invalid', async () => { const response = await request - .patch(`/user/${user.id}`) + .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') - .send({ email: 'johndoe' }); + .send({ + data: { + type: 'users', + attributes: { + email: 'johndoe', + }, + }, + }); - expect(response.status).toEqual(400); - expect(response.body).toEqual({ + expect(response.status).toEqual(StatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ jsonapi: { - version: '1.0', + version: '1.1', }, links: { - self: `/user/${user.id}`, + self: `/users/${user.id}`, }, errors: [ { - status: 400, + status: StatusCodes.BAD_REQUEST, title: 'Bad Request', - detail: 'The email must be a valid email address.', + detail: 'The data.attributes.email must be a valid email address.', }, ], }); @@ -588,38 +580,31 @@ describe('PATCH /user/{id}', () => { test('should not update an user', async () => { const response = await request - .patch(`/user/${user.id}`) - .set('Accept', 'application/vnd.api+json'); - - expect(response.status).toEqual(200); - expect(response.body).toEqual( - expect.objectContaining({ - jsonapi: { - version: '1.0', - }, - links: { - self: `/user/${user.id}`, + .patch(`/users/${user.id}`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + type: 'users', }, - }) - ); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + }); + + expect(response.status).toEqual(StatusCodes.OK); + expect(response.body).toHaveProperty('jsonapi.version', '1.1'); + expect(response.body).toHaveProperty('data.id', user.id); + expect(response.body).toHaveProperty('data.type', 'users'); + expect(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect(response.body).toHaveProperty( 'data.attributes.last_name', user.last_name ); - expect(response.body).toHaveProperty('data.attributes.nickname'); - expect(response.body).toHaveProperty( - 'data.attributes.email', - user.email - ); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + expect(response.body).toHaveProperty('data.attributes.nickname'); + expect(response.body).toHaveProperty('data.attributes.email', user.email); + expect(response.body).toHaveProperty('data.attributes.photo', null); + expect(response.body).toHaveProperty('data.attributes.avatar', null); + expect(response.body).toHaveProperty('data.attributes.created_at'); + expect(response.body).toHaveProperty('data.attributes.updated_at'); }); }); diff --git a/backend/node/database/seeds/users.js b/backend/node/database/seeds/users.js index 0e9aca1..5389532 100644 --- a/backend/node/database/seeds/users.js +++ b/backend/node/database/seeds/users.js @@ -1,5 +1,4 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires -const { hash } = require('argon2'); /** * @param { import("knex").Knex } knex diff --git a/backend/node/jest.config.ts b/backend/node/jest.config.js similarity index 53% rename from backend/node/jest.config.ts rename to backend/node/jest.config.js index 83f2b12..c0ac42f 100644 --- a/backend/node/jest.config.ts +++ b/backend/node/jest.config.js @@ -1,14 +1,14 @@ -import type { InitialOptionsTsJest } from 'ts-jest'; - -const config: InitialOptionsTsJest = { +/** @type {import('jest').Config} */ +const config = { clearMocks: true, collectCoverage: true, coverageDirectory: 'coverage', coverageProvider: 'v8', // globalTeardown: './__tests__/teardown.ts', - preset: 'ts-jest', testMatch: ['**/src/**/*.spec.ts', '**/__tests__/**/*.test.ts'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, }; -// eslint-disable-next-line import/no-default-export -export default config; +module.exports = config; diff --git a/backend/node/package.json b/backend/node/package.json index ffd5a77..ba69338 100644 --- a/backend/node/package.json +++ b/backend/node/package.json @@ -15,45 +15,64 @@ "migrate:down": "NODE_OPTIONS='-r dotenv/config' knex migrate:down", "seed:make": "NODE_OPTIONS='-r dotenv/config' knex seed:make", "seed:run": "NODE_OPTIONS='-r dotenv/config' knex seed:run", - "tsnode-config": "tsc --showConfig > tsconfig.tsnode.json", - "dev:express": "tsnd -r dotenv/config --project tsconfig.tsnode.json src/infrastructure/server/express/index.ts", - "dev": "run-s tsnode-config dev:express", + "dev:express": "node -r dotenv/config -r @swc-node/register --watch src/infrastructure/server/express/index.ts", + "dev": "run-s dev:express", "clean": "rimraf dist", "build": "run-s clean build:ts", - "build:ts": "tsc", + "build:ts": "tsc -p tsconfig.build.json", "start:express": "node dist/infrastructure/server/express/index.js", + "start": "run-s start:express", "lint": "eslint --ignore-path .gitignore --ext .js,.ts .", "lint:fix": "eslint --ignore-path .gitignore --ext .js,.ts --fix .", + "typecheck": "tsc --noEmit", "prettier": "prettier --check .", "prettier:write": "prettier --write .", - "format": "run-s lint:fix prettier:write", - "test": "NODE_ENV=testing vitest run", - "test:watch": "NODE_ENV=testing vitest --watch" + "format": "run-s lint:fix prettier:write typecheck", + "test": "NODE_ENV=testing jest", + "test:watch": "NODE_ENV=testing jest --watch" }, "dependencies": { "@aws-sdk/client-s3": "3.388.0", "@aws-sdk/s3-request-presigner": "3.388.0", "@elastic/ecs-winston-format": "1.3.1", "@lukeed/ms": "2.0.1", - "argon2": "0.31.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", "cache-manager": "5.2.3", "cache-manager-ioredis": "2.1.0", + "cookie-parser": "^1.4.6", + "express": "^4.18.2", + "express-async-handler": "^1.2.0", + "express-json-validator-middleware": "^3.0.1", "fast-jwt": "1.7.2", + "http-status-codes": "^2.3.0", "ioredis": "5.3.2", "knex": "2.5.1", + "multer": "1.4.5-lts.1", "pg": "8.11.2", - "winston": "3.10.0" + "swagger-ui-express": "^5.0.0", + "ts-japi": "^1.9.1", + "winston": "3.10.0", + "yaml": "^2.3.4" }, "devDependencies": { + "@swc-node/register": "^1.6.8", "@swc/core": "^1.3.77", "@swc/helpers": "^0.5.1", - "@tsconfig/node18": "^18.2.0", + "@swc/jest": "^0.2.29", + "@tsconfig/node20": "^20.1.2", "@tsconfig/strictest": "^2.0.1", "@types/cache-manager": "4.0.2", "@types/cache-manager-ioredis": "2.0.3", + "@types/cookie-parser": "^1.4.6", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/multer": "^1.4.11", "@types/node": "18.17.5", "@types/pg": "8.10.2", "@types/supertest": "2.0.12", + "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "dotenv": "16.3.1", @@ -63,14 +82,13 @@ "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-prettier": "4.2.1", + "jest": "^29.7.0", "npm-run-all": "4.1.5", "prettier": "2.8.8", "rimraf": "5.0.1", "supertest": "6.3.3", - "ts-node": "10.9.1", - "ts-node-dev": "2.0.0", - "typescript": "5.1.6", - "vitest": "^0.34.1" + "ts-node": "10.9.2", + "typescript": "5.1.6" }, "volta": { "node": "18.17.1", diff --git a/backend/node/pnpm-lock.yaml b/backend/node/pnpm-lock.yaml index 998f753..ce835f8 100644 --- a/backend/node/pnpm-lock.yaml +++ b/backend/node/pnpm-lock.yaml @@ -4,171 +4,174 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -importers: - - .: - dependencies: - '@aws-sdk/client-s3': - specifier: 3.388.0 - version: 3.388.0 - '@aws-sdk/s3-request-presigner': - specifier: 3.388.0 - version: 3.388.0 - '@elastic/ecs-winston-format': - specifier: 1.3.1 - version: 1.3.1 - '@lukeed/ms': - specifier: 2.0.1 - version: 2.0.1 - argon2: - specifier: 0.31.0 - version: 0.31.0 - cache-manager: - specifier: 5.2.3 - version: 5.2.3 - cache-manager-ioredis: - specifier: 2.1.0 - version: 2.1.0 - fast-jwt: - specifier: 1.7.2 - version: 1.7.2 - ioredis: - specifier: 5.3.2 - version: 5.3.2 - knex: - specifier: 2.5.1 - version: 2.5.1(pg@8.11.2) - pg: - specifier: 8.11.2 - version: 8.11.2 - winston: - specifier: 3.10.0 - version: 3.10.0 - devDependencies: - '@swc/core': - specifier: ^1.3.77 - version: 1.3.96(@swc/helpers@0.5.3) - '@swc/helpers': - specifier: ^0.5.1 - version: 0.5.3 - '@tsconfig/node18': - specifier: ^18.2.0 - version: 18.2.2 - '@tsconfig/strictest': - specifier: ^2.0.1 - version: 2.0.2 - '@types/cache-manager': - specifier: 4.0.2 - version: 4.0.2 - '@types/cache-manager-ioredis': - specifier: 2.0.3 - version: 2.0.3 - '@types/node': - specifier: 18.17.5 - version: 18.17.5 - '@types/pg': - specifier: 8.10.2 - version: 8.10.2 - '@types/supertest': - specifier: 2.0.12 - version: 2.0.12 - '@typescript-eslint/eslint-plugin': - specifier: 5.62.0 - version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.47.0)(typescript@5.1.6) - '@typescript-eslint/parser': - specifier: 5.62.0 - version: 5.62.0(eslint@8.47.0)(typescript@5.1.6) - dotenv: - specifier: 16.3.1 - version: 16.3.1 - eslint: - specifier: 8.47.0 - version: 8.47.0 - eslint-config-airbnb-base: - specifier: 15.0.0 - version: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.47.0) - eslint-config-airbnb-typescript: - specifier: 17.1.0 - version: 17.1.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.26.0)(eslint@8.47.0) - eslint-config-prettier: - specifier: 9.0.0 - version: 9.0.0(eslint@8.47.0) - eslint-plugin-import: - specifier: 2.26.0 - version: 2.26.0(@typescript-eslint/parser@5.62.0)(eslint@8.47.0) - eslint-plugin-prettier: - specifier: 4.2.1 - version: 4.2.1(eslint-config-prettier@9.0.0)(eslint@8.47.0)(prettier@2.8.8) - npm-run-all: - specifier: 4.1.5 - version: 4.1.5 - prettier: - specifier: 2.8.8 - version: 2.8.8 - rimraf: - specifier: 5.0.1 - version: 5.0.1 - supertest: - specifier: 6.3.3 - version: 6.3.3 - ts-node: - specifier: 10.9.1 - version: 10.9.1(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6) - ts-node-dev: - specifier: 2.0.0 - version: 2.0.0(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6) - typescript: - specifier: 5.1.6 - version: 5.1.6 - vitest: - specifier: ^0.34.1 - version: 0.34.6 - - src/infrastructure/server/express: - dependencies: - ajv: - specifier: ^8.11.0 - version: 8.12.0 - ajv-errors: - specifier: 3.0.0 - version: 3.0.0(ajv@8.12.0) - ajv-formats: - specifier: 2.1.1 - version: 2.1.1(ajv@8.12.0) - cookie-parser: - specifier: 1.4.6 - version: 1.4.6 - express: - specifier: 4.18.2 - version: 4.18.2 - express-async-handler: - specifier: 1.2.0 - version: 1.2.0 - express-json-validator-middleware: - specifier: 3.0.1 - version: 3.0.1 - multer: - specifier: 1.4.5-lts.1 - version: 1.4.5-lts.1 - swagger-ui-express: - specifier: 5.0.0 - version: 5.0.0(express@4.18.2) - yaml: - specifier: 2.3.1 - version: 2.3.1 - devDependencies: - '@types/cookie-parser': - specifier: 1.4.3 - version: 1.4.3 - '@types/express': - specifier: 4.17.17 - version: 4.17.17 - '@types/multer': - specifier: 1.4.7 - version: 1.4.7 - '@types/swagger-ui-express': - specifier: 4.1.3 - version: 4.1.3 +dependencies: + '@aws-sdk/client-s3': + specifier: 3.388.0 + version: 3.388.0 + '@aws-sdk/s3-request-presigner': + specifier: 3.388.0 + version: 3.388.0 + '@elastic/ecs-winston-format': + specifier: 1.3.1 + version: 1.3.1 + '@lukeed/ms': + specifier: 2.0.1 + version: 2.0.1 + ajv: + specifier: ^8.12.0 + version: 8.12.0 + ajv-errors: + specifier: ^3.0.0 + version: 3.0.0(ajv@8.12.0) + ajv-formats: + specifier: ^2.1.1 + version: 2.1.1(ajv@8.12.0) + cache-manager: + specifier: 5.2.3 + version: 5.2.3 + cache-manager-ioredis: + specifier: 2.1.0 + version: 2.1.0 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.6 + express: + specifier: ^4.18.2 + version: 4.18.2 + express-async-handler: + specifier: ^1.2.0 + version: 1.2.0 + express-json-validator-middleware: + specifier: ^3.0.1 + version: 3.0.1 + fast-jwt: + specifier: 1.7.2 + version: 1.7.2 + http-status-codes: + specifier: ^2.3.0 + version: 2.3.0 + ioredis: + specifier: 5.3.2 + version: 5.3.2 + knex: + specifier: 2.5.1 + version: 2.5.1(pg@8.11.2) + multer: + specifier: 1.4.5-lts.1 + version: 1.4.5-lts.1 + pg: + specifier: 8.11.2 + version: 8.11.2 + swagger-ui-express: + specifier: ^5.0.0 + version: 5.0.0(express@4.18.2) + ts-japi: + specifier: ^1.9.1 + version: 1.9.1 + winston: + specifier: 3.10.0 + version: 3.10.0 + yaml: + specifier: ^2.3.4 + version: 2.3.4 + +devDependencies: + '@swc-node/register': + specifier: ^1.6.8 + version: 1.6.8(@swc/core@1.3.96)(typescript@5.1.6) + '@swc/core': + specifier: ^1.3.77 + version: 1.3.96(@swc/helpers@0.5.3) + '@swc/helpers': + specifier: ^0.5.1 + version: 0.5.3 + '@swc/jest': + specifier: ^0.2.29 + version: 0.2.29(@swc/core@1.3.96) + '@tsconfig/node20': + specifier: ^20.1.2 + version: 20.1.2 + '@tsconfig/strictest': + specifier: ^2.0.1 + version: 2.0.2 + '@types/cache-manager': + specifier: 4.0.2 + version: 4.0.2 + '@types/cache-manager-ioredis': + specifier: 2.0.3 + version: 2.0.3 + '@types/cookie-parser': + specifier: ^1.4.6 + version: 1.4.6 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/jest': + specifier: ^29.5.11 + version: 29.5.11 + '@types/multer': + specifier: ^1.4.11 + version: 1.4.11 + '@types/node': + specifier: 18.17.5 + version: 18.17.5 + '@types/pg': + specifier: 8.10.2 + version: 8.10.2 + '@types/supertest': + specifier: 2.0.12 + version: 2.0.12 + '@types/swagger-ui-express': + specifier: ^4.1.6 + version: 4.1.6 + '@typescript-eslint/eslint-plugin': + specifier: 5.62.0 + version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.47.0)(typescript@5.1.6) + '@typescript-eslint/parser': + specifier: 5.62.0 + version: 5.62.0(eslint@8.47.0)(typescript@5.1.6) + dotenv: + specifier: 16.3.1 + version: 16.3.1 + eslint: + specifier: 8.47.0 + version: 8.47.0 + eslint-config-airbnb-base: + specifier: 15.0.0 + version: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.47.0) + eslint-config-airbnb-typescript: + specifier: 17.1.0 + version: 17.1.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.26.0)(eslint@8.47.0) + eslint-config-prettier: + specifier: 9.0.0 + version: 9.0.0(eslint@8.47.0) + eslint-plugin-import: + specifier: 2.26.0 + version: 2.26.0(@typescript-eslint/parser@5.62.0)(eslint@8.47.0) + eslint-plugin-prettier: + specifier: 4.2.1 + version: 4.2.1(eslint-config-prettier@9.0.0)(eslint@8.47.0)(prettier@2.8.8) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.17.5)(ts-node@10.9.2) + npm-run-all: + specifier: 4.1.5 + version: 4.1.5 + prettier: + specifier: 2.8.8 + version: 2.8.8 + rimraf: + specifier: 5.0.1 + version: 5.0.1 + supertest: + specifier: 6.3.3 + version: 6.3.3 + ts-node: + specifier: 10.9.2 + version: 10.9.2(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6) + typescript: + specifier: 5.1.6 + version: 5.1.6 packages: @@ -177,6 +180,14 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.21 + dev: true + /@aws-crypto/crc32@3.0.0: resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} dependencies: @@ -758,242 +769,373 @@ packages: tslib: 2.6.2 dev: false - /@colors/colors@1.5.0: - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - dev: false + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true - /@colors/colors@1.6.0: - resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} - engines: {node: '>=0.1.90'} - dev: false + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + dev: true - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + /@babel/core@7.23.7: + resolution: {integrity: sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==} + engines: {node: '>=6.9.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.9 + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helpers': 7.23.8 + '@babel/parser': 7.23.6 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.7 + '@babel/types': 7.23.6 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color dev: true - /@dabh/diagnostics@2.0.3: - resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} dependencies: - colorspace: 1.1.4 - enabled: 2.0.0 - kuler: 2.0.0 - dev: false + '@babel/types': 7.23.6 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.21 + jsesc: 2.5.2 + dev: true - /@elastic/ecs-helpers@1.1.0: - resolution: {integrity: sha512-MDLb2aFeGjg46O5mLpdCzT5yOUDnXToJSrco2ShqGIXxNJaM8uJjX+4nd+hRYV4Vex8YJyDtOFEVBldQct6ndg==} - engines: {node: '>=10'} + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} dependencies: - fast-json-stringify: 2.7.13 - dev: false + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.22.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true - /@elastic/ecs-winston-format@1.3.1: - resolution: {integrity: sha512-cbDaTU6zUXNpAZSJoLUgNqB0kq2YZ1hmDePVdKrJmw7OBDbzUUMfaK+7S21yFpln/tUF4KRSSPbSwEBgtTLp7Q==} - engines: {node: '>=10'} + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} dependencies: - '@elastic/ecs-helpers': 1.1.0 - dev: false + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + dev: true - /@esbuild/android-arm64@0.19.5: - resolution: {integrity: sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 dev: true - optional: true - /@esbuild/android-arm@0.19.5: - resolution: {integrity: sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 dev: true - optional: true - /@esbuild/android-x64@0.19.5: - resolution: {integrity: sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 dev: true - optional: true - /@esbuild/darwin-arm64@0.19.5: - resolution: {integrity: sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} dev: true - optional: true - /@esbuild/darwin-x64@0.19.5: - resolution: {integrity: sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 dev: true - optional: true - /@esbuild/freebsd-arm64@0.19.5: - resolution: {integrity: sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 dev: true - optional: true - /@esbuild/freebsd-x64@0.19.5: - resolution: {integrity: sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} dev: true - optional: true - /@esbuild/linux-arm64@0.19.5: - resolution: {integrity: sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} dev: true - optional: true - /@esbuild/linux-arm@0.19.5: - resolution: {integrity: sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} dev: true - optional: true - /@esbuild/linux-ia32@0.19.5: - resolution: {integrity: sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true + /@babel/helpers@7.23.8: + resolution: {integrity: sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.7 + '@babel/types': 7.23.6 + transitivePeerDependencies: + - supports-color dev: true - optional: true - /@esbuild/linux-loong64@0.19.5: - resolution: {integrity: sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 dev: true - optional: true - /@esbuild/linux-mips64el@0.19.5: - resolution: {integrity: sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true + /@babel/parser@7.23.6: + resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 dev: true - optional: true - /@esbuild/linux-ppc64@0.19.5: - resolution: {integrity: sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.7): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/linux-riscv64@0.19.5: - resolution: {integrity: sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/linux-s390x@0.19.5: - resolution: {integrity: sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.7): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/linux-x64@0.19.5: - resolution: {integrity: sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.7): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/netbsd-x64@0.19.5: - resolution: {integrity: sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/openbsd-x64@0.19.5: - resolution: {integrity: sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/sunos-x64@0.19.5: - resolution: {integrity: sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.7): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/win32-arm64@0.19.5: - resolution: {integrity: sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/win32-ia32@0.19.5: - resolution: {integrity: sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.7): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 dev: true - optional: true - /@esbuild/win32-x64@0.19.5: - resolution: {integrity: sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==} + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.7): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.7): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + dev: true + + /@babel/traverse@7.23.7: + resolution: {integrity: sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.6: + resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + dev: false + + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true + dependencies: + '@jridgewell/trace-mapping': 0.3.9 dev: true - optional: true + + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + + /@elastic/ecs-helpers@1.1.0: + resolution: {integrity: sha512-MDLb2aFeGjg46O5mLpdCzT5yOUDnXToJSrco2ShqGIXxNJaM8uJjX+4nd+hRYV4Vex8YJyDtOFEVBldQct6ndg==} + engines: {node: '>=10'} + dependencies: + fast-json-stringify: 2.7.13 + dev: false + + /@elastic/ecs-winston-format@1.3.1: + resolution: {integrity: sha512-cbDaTU6zUXNpAZSJoLUgNqB0kq2YZ1hmDePVdKrJmw7OBDbzUUMfaK+7S21yFpln/tUF4KRSSPbSwEBgtTLp7Q==} + engines: {node: '>=10'} + dependencies: + '@elastic/ecs-helpers': 1.1.0 + dev: false /@eslint-community/eslint-utils@4.4.0(eslint@8.47.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} @@ -1068,6 +1210,172 @@ packages: wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/core@29.7.0(ts-node@10.9.2): + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.17.5)(ts-node@10.9.2) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /@jest/create-cache-key-function@27.5.1: + resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + dev: true + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + jest-mock: 29.7.0 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.17.5 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.21 + '@types/node': 18.17.5 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1075,15 +1383,111 @@ packages: '@sinclair/typebox': 0.27.8 dev: true + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.21 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.7 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.21 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@27.5.1: + resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.17.5 + '@types/yargs': 16.0.9 + chalk: 4.1.2 + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.17.5 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.21 + dev: true + /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} dev: true + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true + /@jridgewell/trace-mapping@0.3.21: + resolution: {integrity: sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -1096,24 +1500,6 @@ packages: engines: {node: '>=8'} dev: false - /@mapbox/node-pre-gyp@1.0.11: - resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} - hasBin: true - dependencies: - detect-libc: 2.0.2 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 - node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.5.4 - tar: 6.2.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1135,11 +1521,6 @@ packages: fastq: 1.15.0 dev: true - /@phc/format@1.0.0: - resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} - engines: {node: '>=10'} - dev: false - /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1147,104 +1528,20 @@ packages: dev: true optional: true - /@rollup/rollup-android-arm-eabi@4.4.1: - resolution: {integrity: sha512-Ss4suS/sd+6xLRu+MLCkED2mUrAyqHmmvZB+zpzZ9Znn9S8wCkTQCJaQ8P8aHofnvG5L16u9MVnJjCqioPErwQ==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm64@4.4.1: - resolution: {integrity: sha512-sRSkGTvGsARwWd7TzC8LKRf8FiPn7257vd/edzmvG4RIr9x68KBN0/Ek48CkuUJ5Pj/Dp9vKWv6PEupjKWjTYA==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-arm64@4.4.1: - resolution: {integrity: sha512-nz0AiGrrXyaWpsmBXUGOBiRDU0wyfSXbFuF98pPvIO8O6auQsPG6riWsfQqmCCC5FNd8zKQ4JhgugRNAkBJ8mQ==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-x64@4.4.1: - resolution: {integrity: sha512-Ogqvf4/Ve/faMaiPRvzsJEqajbqs00LO+8vtrPBVvLgdw4wBg6ZDXdkDAZO+4MLnrc8mhGV6VJAzYScZdPLtJg==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-gnueabihf@4.4.1: - resolution: {integrity: sha512-9zc2tqlr6HfO+hx9+wktUlWTRdje7Ub15iJqKcqg5uJZ+iKqmd2CMxlgPpXi7+bU7bjfDIuvCvnGk7wewFEhCg==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-gnu@4.4.1: - resolution: {integrity: sha512-phLb1fN3rq2o1j1v+nKxXUTSJnAhzhU0hLrl7Qzb0fLpwkGMHDem+o6d+ZI8+/BlTXfMU4kVWGvy6g9k/B8L6Q==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-musl@4.4.1: - resolution: {integrity: sha512-M2sDtw4tf57VPSjbTAN/lz1doWUqO2CbQuX3L9K6GWIR5uw9j+ROKCvvUNBY8WUbMxwaoc8mH9HmmBKsLht7+w==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-gnu@4.4.1: - resolution: {integrity: sha512-mHIlRLX+hx+30cD6c4BaBOsSqdnCE4ok7/KDvjHYAHoSuveoMMxIisZFvcLhUnyZcPBXDGZTuBoalcuh43UfQQ==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-musl@4.4.1: - resolution: {integrity: sha512-tB+RZuDi3zxFx7vDrjTNGVLu2KNyzYv+UY8jz7e4TMEoAj7iEt8Qk6xVu6mo3pgjnsHj6jnq3uuRsHp97DLwOA==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-arm64-msvc@4.4.1: - resolution: {integrity: sha512-Hdn39PzOQowK/HZzYpCuZdJC91PE6EaGbTe2VCA9oq2u18evkisQfws0Smh9QQGNNRa/T7MOuGNQoLeXhhE3PQ==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-ia32-msvc@4.4.1: - resolution: {integrity: sha512-tLpKb1Elm9fM8c5w3nl4N1eLTP4bCqTYw9tqUBxX8/hsxqHO3dxc2qPbZ9PNkdK4tg4iLEYn0pOUnVByRd2CbA==} - cpu: [ia32] - os: [win32] - requiresBuild: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - optional: true - /@rollup/rollup-win32-x64-msvc@4.4.1: - resolution: {integrity: sha512-eAhItDX9yQtZVM3yvXS/VR3qPqcnXvnLyx1pLXl4JzyNMBNO3KC986t/iAg2zcMzpAp9JSvxB5VZGnBiNoA98w==} - cpu: [x64] - os: [win32] - requiresBuild: true + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 dev: true - optional: true - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 dev: true /@smithy/abort-controller@2.0.13: @@ -1684,6 +1981,40 @@ packages: tslib: 2.6.2 dev: false + /@swc-node/core@1.10.6(@swc/core@1.3.96): + resolution: {integrity: sha512-lDIi/rPosmKIknWzvs2/Fi9zWRtbkx8OJ9pQaevhsoGzJSal8Pd315k1W5AIrnknfdAB4HqRN12fk6AhqnrEEw==} + engines: {node: '>= 10'} + peerDependencies: + '@swc/core': '>= 1.3' + dependencies: + '@swc/core': 1.3.96(@swc/helpers@0.5.3) + dev: true + + /@swc-node/register@1.6.8(@swc/core@1.3.96)(typescript@5.1.6): + resolution: {integrity: sha512-74ijy7J9CWr1Z88yO+ykXphV29giCrSpANQPQRooE0bObpkTO1g4RzQovIfbIaniBiGDDVsYwDoQ3FIrCE8HcQ==} + peerDependencies: + '@swc/core': '>= 1.3' + typescript: '>= 4.3' + dependencies: + '@swc-node/core': 1.10.6(@swc/core@1.3.96) + '@swc-node/sourcemap-support': 0.3.0 + '@swc/core': 1.3.96(@swc/helpers@0.5.3) + colorette: 2.0.19 + debug: 4.3.4 + pirates: 4.0.6 + tslib: 2.6.2 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@swc-node/sourcemap-support@0.3.0: + resolution: {integrity: sha512-gqBJSmJMWomZFxlppaKea7NeAqFrDrrS0RMt24No92M3nJWcyI9YKGEQKl+EyJqZ5gh6w1s0cTklMHMzRwA1NA==} + dependencies: + source-map-support: 0.5.21 + tslib: 2.6.2 + dev: true + /@swc/core-darwin-arm64@1.3.96: resolution: {integrity: sha512-8hzgXYVd85hfPh6mJ9yrG26rhgzCmcLO0h1TIl8U31hwmTbfZLzRitFQ/kqMJNbIBCwmNH1RU2QcJnL3d7f69A==} engines: {node: '>=10'} @@ -1810,6 +2141,17 @@ packages: tslib: 2.6.2 dev: true + /@swc/jest@0.2.29(@swc/core@1.3.96): + resolution: {integrity: sha512-8reh5RvHBsSikDC3WGCd5ZTd2BXKkyOdK7QwynrCH58jk2cQFhhHhFBg/jvnWZehUQe/EoOImLENc9/DwbBFow==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + dependencies: + '@jest/create-cache-key-function': 27.5.1 + '@swc/core': 1.3.96(@swc/helpers@0.5.3) + jsonc-parser: 3.2.0 + dev: true + /@swc/types@0.1.5: resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==} dev: true @@ -1830,14 +2172,43 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} dev: true - /@tsconfig/node18@18.2.2: - resolution: {integrity: sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==} + /@tsconfig/node20@20.1.2: + resolution: {integrity: sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==} dev: true /@tsconfig/strictest@2.0.2: resolution: {integrity: sha512-jt4jIsWKvUvuY6adJnQJlb/UR7DdjC8CjHI/OaSQruj2yX9/K6+KOvDt/vD6udqos/FUk5Op66CvYT7TBLYO5Q==} dev: true + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.5 + dev: true + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + dev: true + + /@types/babel__traverse@7.20.5: + resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + dependencies: + '@babel/types': 7.23.6 + dev: true + /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: @@ -1855,25 +2226,15 @@ packages: resolution: {integrity: sha512-fT5FMdzsiSX0AbgnS5gDvHl2Nco0h5zYyjwDQy4yPC7Ww6DeGMVKPRqIZtg9HOXDV2kkc18SL1B0N8f0BecrCA==} dev: true - /@types/chai-subset@1.3.5: - resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==} - dependencies: - '@types/chai': 4.3.10 - dev: true - - /@types/chai@4.3.10: - resolution: {integrity: sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==} - dev: true - - /@types/connect@3.4.38: - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: '@types/node': 18.17.5 - /@types/cookie-parser@1.4.3: - resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==} + /@types/cookie-parser@1.4.6: + resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.21 dev: true /@types/cookiejar@2.1.4: @@ -1888,14 +2249,20 @@ packages: '@types/range-parser': 1.2.7 '@types/send': 0.17.4 - /@types/express@4.17.17: - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.5 '@types/express-serve-static-core': 4.17.41 '@types/qs': 6.9.10 '@types/serve-static': 1.15.5 + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 18.17.5 + dev: true + /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} @@ -1905,6 +2272,29 @@ packages: '@types/node': 18.17.5 dev: true + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/jest@29.5.11: + resolution: {integrity: sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1918,10 +2308,10 @@ packages: /@types/mime@3.0.4: resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} - /@types/multer@1.4.7: - resolution: {integrity: sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==} + /@types/multer@1.4.11: + resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.21 dev: true /@types/node@18.17.5: @@ -1958,12 +2348,8 @@ packages: '@types/mime': 3.0.4 '@types/node': 18.17.5 - /@types/strip-bom@3.0.0: - resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} - dev: true - - /@types/strip-json-comments@0.0.30: - resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true /@types/superagent@4.1.22: @@ -1979,10 +2365,10 @@ packages: '@types/superagent': 4.1.22 dev: true - /@types/swagger-ui-express@4.1.3: - resolution: {integrity: sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==} + /@types/swagger-ui-express@4.1.6: + resolution: {integrity: sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.21 '@types/serve-static': 1.15.5 dev: true @@ -1990,6 +2376,22 @@ packages: resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} dev: false + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@16.0.9: + resolution: {integrity: sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.47.0)(typescript@5.1.6): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2120,48 +2522,6 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@vitest/expect@0.34.6: - resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} - dependencies: - '@vitest/spy': 0.34.6 - '@vitest/utils': 0.34.6 - chai: 4.3.10 - dev: true - - /@vitest/runner@0.34.6: - resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} - dependencies: - '@vitest/utils': 0.34.6 - p-limit: 4.0.0 - pathe: 1.1.1 - dev: true - - /@vitest/snapshot@0.34.6: - resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} - dependencies: - magic-string: 0.30.5 - pathe: 1.1.1 - pretty-format: 29.7.0 - dev: true - - /@vitest/spy@0.34.6: - resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} - dependencies: - tinyspy: 2.2.0 - dev: true - - /@vitest/utils@0.34.6: - resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} - dependencies: - diff-sequences: 29.6.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - dev: true - - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: false - /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -2189,15 +2549,6 @@ packages: hasBin: true dev: true - /agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: false - /ajv-errors@3.0.0(ajv@8.12.0): resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} peerDependencies: @@ -2234,9 +2585,17 @@ packages: uri-js: 4.4.1 dev: false + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + dev: true /ansi-regex@6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} @@ -2279,34 +2638,15 @@ packages: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} dev: false - /aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - dev: false - - /are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - dev: false - /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true - /argon2@0.31.0: - resolution: {integrity: sha512-r56NWwlE3tjD/FIqL1T+V4Ka+Mb5yMF35w1YWHpwpEjeONXBUbxmjhWkWqY63mse8lpcZ+ZZIGpKL+s+qXhyfg==} - engines: {node: '>=14.0.0'} - requiresBuild: true + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - '@phc/format': 1.0.0 - node-addon-api: 7.0.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: false + sprintf-js: 1.0.3 + dev: true /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2375,10 +2715,6 @@ packages: safer-buffer: 2.1.2 dev: false - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true - /async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} dev: false @@ -2392,12 +2728,80 @@ packages: engines: {node: '>= 0.4'} dev: true - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /babel-jest@29.7.0(@babel/core@7.23.7): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.23.7 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.23.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.22.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.5 + dev: true + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.7): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.7) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.23.7): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.7) + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true /bn.js@4.12.0: @@ -2433,6 +2837,7 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + dev: true /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -2447,6 +2852,23 @@ packages: fill-range: 7.0.1 dev: true + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001576 + electron-to-chromium: 1.4.630 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2467,11 +2889,6 @@ packages: engines: {node: '>= 0.8'} dev: false - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true - /cache-manager-ioredis@2.1.0: resolution: {integrity: sha512-TCxbp9ceuFveTKWuNaCX8QjoC41rAlHen4s63u9Yd+iXlw3efYmimc/u935PKPxSdhkXpnMes4mxtK3/yb0L4g==} engines: {node: '>=6.0.0'} @@ -2500,17 +2917,18 @@ packages: engines: {node: '>=6'} dev: true - /chai@4.3.10: - resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.3 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.0.8 + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /caniuse-lite@1.0.30001576: + resolution: {integrity: sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==} dev: true /chalk@2.4.2: @@ -2530,37 +2948,43 @@ packages: supports-color: 7.2.0 dev: true - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - dependencies: - get-func-name: 2.0.2 + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} dev: true - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} dev: true - /chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - dev: false + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} dev: false + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + dev: true + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -2586,11 +3010,6 @@ packages: simple-swizzle: 0.2.2 dev: false - /color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - dev: false - /color@3.2.1: resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} dependencies: @@ -2600,7 +3019,6 @@ packages: /colorette@2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} - dev: false /colorspace@1.1.4: resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} @@ -2627,6 +3045,7 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true /concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -2642,10 +3061,6 @@ packages: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} dev: true - /console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - dev: false - /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -2658,6 +3073,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + /cookie-parser@1.4.6: resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} engines: {node: '>= 0.8.0'} @@ -2688,6 +3107,25 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: false + /create-jest@29.7.0(@types/node@18.17.5)(ts-node@10.9.2): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.17.5)(ts-node@10.9.2) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -2744,11 +3182,13 @@ packages: dependencies: ms: 2.1.2 - /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} - dependencies: - type-detect: 4.0.8 + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true dev: true /deep-is@0.1.4: @@ -2758,7 +3198,6 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: false /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} @@ -2782,10 +3221,6 @@ packages: engines: {node: '>=0.4.0'} dev: true - /delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dev: false - /denque@1.5.1: resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} engines: {node: '>=0.10'} @@ -2806,10 +3241,10 @@ packages: engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dev: false - /detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - dev: false + dev: true /dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -2854,12 +3289,6 @@ packages: engines: {node: '>=12'} dev: true - /dynamic-dedupe@0.3.0: - resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} - dependencies: - xtend: 4.0.2 - dev: true - /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -2874,8 +3303,18 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false + /electron-to-chromium@1.4.630: + resolution: {integrity: sha512-osHqhtjojpCsACVnuD11xO5g9xaCyw7Qqn/C2KParkMv42i8jrJJgx3g7mkHfpxwhy9MnOJr8+pKOdZ7qzgizg==} + dev: true + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -2965,40 +3404,9 @@ packages: is-symbol: 1.0.4 dev: true - /esbuild@0.19.5: - resolution: {integrity: sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.19.5 - '@esbuild/android-arm64': 0.19.5 - '@esbuild/android-x64': 0.19.5 - '@esbuild/darwin-arm64': 0.19.5 - '@esbuild/darwin-x64': 0.19.5 - '@esbuild/freebsd-arm64': 0.19.5 - '@esbuild/freebsd-x64': 0.19.5 - '@esbuild/linux-arm': 0.19.5 - '@esbuild/linux-arm64': 0.19.5 - '@esbuild/linux-ia32': 0.19.5 - '@esbuild/linux-loong64': 0.19.5 - '@esbuild/linux-mips64el': 0.19.5 - '@esbuild/linux-ppc64': 0.19.5 - '@esbuild/linux-riscv64': 0.19.5 - '@esbuild/linux-s390x': 0.19.5 - '@esbuild/linux-x64': 0.19.5 - '@esbuild/netbsd-x64': 0.19.5 - '@esbuild/openbsd-x64': 0.19.5 - '@esbuild/sunos-x64': 0.19.5 - '@esbuild/win32-arm64': 0.19.5 - '@esbuild/win32-ia32': 0.19.5 - '@esbuild/win32-x64': 0.19.5 - dev: true - /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} - dev: false /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -3009,6 +3417,11 @@ packages: engines: {node: '>=0.8.0'} dev: true + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -3221,6 +3634,12 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} @@ -3255,6 +3674,37 @@ packages: engines: {node: '>= 0.6'} dev: false + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: true + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + /express-async-handler@1.2.0: resolution: {integrity: sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==} dev: false @@ -3263,7 +3713,7 @@ packages: resolution: {integrity: sha512-DkqrIwS4O1eCqshuBNG76dBV+BuoXhgsiUjW9Rh3aBerlIY6fwIrNQ+UZLqh6CLbqcQRQTbqiNA5AlneqlUflA==} engines: {node: '>=14'} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.21 '@types/express-serve-static-core': 4.17.41 '@types/json-schema': 7.0.15 ajv: 8.12.0 @@ -3369,6 +3819,12 @@ packages: reusify: 1.0.4 dev: true + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + /fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} dev: false @@ -3402,6 +3858,14 @@ packages: - supports-color dev: false + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -3469,15 +3933,9 @@ packages: engines: {node: '>= 0.6'} dev: false - /fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: false - /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -3504,23 +3962,14 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true - /gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - dev: false + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} dev: true /get-intrinsic@1.2.2: @@ -3534,7 +3983,11 @@ packages: /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - dev: false + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} @@ -3583,6 +4036,12 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true /globals@13.23.0: resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} @@ -3657,10 +4116,6 @@ packages: has-symbols: 1.0.3 dev: true - /has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - dev: false - /has@1.0.4: resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} engines: {node: '>= 0.4.0'} @@ -3681,6 +4136,10 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -3692,16 +4151,15 @@ packages: toidentifier: 1.0.1 dev: false - /https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color + /http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} dev: false + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3722,6 +4180,15 @@ packages: resolve-from: 4.0.0 dev: true + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -3732,6 +4199,7 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 + dev: true /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3813,13 +4281,6 @@ packages: has-bigints: 1.0.2 dev: true - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} @@ -3853,6 +4314,12 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + dev: true + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: true /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -3900,7 +4367,6 @@ packages: /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - dev: false /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} @@ -3941,6 +4407,65 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.23.7 + '@babel/parser': 7.23.6 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@6.0.1: + resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.23.7 + '@babel/parser': 7.23.6 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -3950,6 +4475,429 @@ packages: '@pkgjs/parseargs': 0.11.0 dev: true + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.4 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.7.0(@types/node@18.17.5)(ts-node@10.9.2): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.17.5)(ts-node@10.9.2) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.17.5)(ts-node@10.9.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jest-config@29.7.0(@types/node@18.17.5)(ts-node@10.9.2): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.23.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + babel-jest: 29.7.0(@babel/core@7.23.7) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.2(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 18.17.5 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.23.5 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + jest-util: 29.7.0 + dev: true + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.7 + '@babel/generator': 7.23.6 + '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.7) + '@babel/types': 7.23.6 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.7) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.17.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 18.17.5 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.7.0(@types/node@18.17.5)(ts-node@10.9.2): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.17.5)(ts-node@10.9.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -3957,6 +4905,12 @@ packages: argparse: 2.0.1 dev: true + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true @@ -3965,6 +4919,10 @@ packages: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} dev: true + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3983,6 +4941,12 @@ packages: minimist: 1.2.8 dev: true + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + /jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true @@ -3993,6 +4957,11 @@ packages: json-buffer: 3.0.1 dev: true + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + /knex@2.5.1(pg@8.11.2): resolution: {integrity: sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==} engines: {node: '>=12'} @@ -4044,6 +5013,11 @@ packages: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} dev: false + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -4052,6 +5026,10 @@ packages: type-check: 0.4.0 dev: true + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + /load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -4062,9 +5040,11 @@ packages: strip-bom: 3.0.0 dev: true - /local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} - engines: {node: '>=14'} + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 dev: true /locate-path@6.0.0: @@ -4110,12 +5090,6 @@ packages: triple-beam: 1.4.1 dev: false - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - dependencies: - get-func-name: 2.0.2 - dev: true - /lru-cache@10.0.2: resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==} engines: {node: 14 || >=16.14} @@ -4123,35 +5097,41 @@ packages: semver: 7.5.4 dev: true + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} dependencies: yallist: 4.0.0 + dev: true /lru-cache@9.1.2: resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==} engines: {node: 14 || >=16.14} dev: false - /magic-string@0.30.5: - resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} - engines: {node: '>=12'} + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + semver: 7.5.4 dev: true - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - dependencies: - semver: 6.3.1 - dev: false - /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4166,6 +5146,10 @@ packages: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} dev: false + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4205,6 +5189,11 @@ packages: hasBin: true dev: true + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} dev: false @@ -4213,6 +5202,7 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + dev: true /minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} @@ -4224,31 +5214,11 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: false - - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - dev: false - /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} engines: {node: '>=16 || 14 >=14.17'} dev: true - /minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - dev: false - /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -4256,20 +5226,6 @@ packages: minimist: 1.2.8 dev: false - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - - /mlly@1.4.2: - resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} - dependencies: - acorn: 8.11.2 - pathe: 1.1.1 - pkg-types: 1.0.3 - ufo: 1.3.2 - dev: true - /mnemonist@0.39.5: resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} dependencies: @@ -4298,12 +5254,6 @@ packages: xtend: 4.0.2 dev: false - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -4317,33 +5267,17 @@ packages: engines: {node: '>= 0.6'} dev: false - /nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - dev: true - - /node-addon-api@7.0.0: - resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} - dev: false - - /node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - - /nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: false + /nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: true + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -4375,14 +5309,12 @@ packages: string.prototype.padend: 3.1.5 dev: true - /npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - dev: false + path-key: 3.1.1 + dev: true /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -4444,6 +5376,7 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 + dev: true /one-time@1.0.0: resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} @@ -4451,6 +5384,13 @@ packages: fn.name: 1.1.0 dev: false + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -4463,6 +5403,13 @@ packages: type-check: 0.4.0 dev: true + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -4470,11 +5417,11 @@ packages: yocto-queue: 0.1.0 dev: true - /p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} dependencies: - yocto-queue: 1.0.0 + p-limit: 2.3.0 dev: true /p-locate@5.0.0: @@ -4489,6 +5436,11 @@ packages: engines: {node: '>=6'} dev: false + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + /packet-reader@1.0.0: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} dev: false @@ -4508,6 +5460,16 @@ packages: json-parse-better-errors: 1.0.2 dev: true + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4521,6 +5483,7 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + dev: true /path-key@2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} @@ -4559,14 +5522,6 @@ packages: engines: {node: '>=8'} dev: true - /pathe@1.1.1: - resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} - dev: true - - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true - /pg-cloudflare@1.1.1: resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} requiresBuild: true @@ -4671,21 +5626,16 @@ packages: engines: {node: '>=4'} dev: true - /pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} - dependencies: - jsonc-parser: 3.2.0 - mlly: 1.4.2 - pathe: 1.1.1 + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} dev: true - /postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.0.2 + find-up: 4.1.0 dev: true /postgres-array@2.0.0: @@ -4767,6 +5717,14 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: false + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4779,6 +5737,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + /pure-rand@6.0.4: + resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + dev: true + /qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -4846,13 +5808,6 @@ packages: util-deprecate: 1.0.2 dev: false - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - /rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} @@ -4885,11 +5840,23 @@ packages: set-function-name: 2.0.1 dev: true + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} dev: false + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4898,7 +5865,11 @@ packages: /resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - dev: false + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} @@ -4917,18 +5888,12 @@ packages: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} dev: false - /rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: glob: 7.2.3 + dev: true /rimraf@5.0.1: resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==} @@ -4938,26 +5903,6 @@ packages: glob: 10.3.10 dev: true - /rollup@4.4.1: - resolution: {integrity: sha512-idZzrUpWSblPJX66i+GzrpjKE3vbYrlWirUHteoAbjKReZwa0cohAErOYA5efoMmNCdvG9yrJS+w9Kl6csaH4w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.4.1 - '@rollup/rollup-android-arm64': 4.4.1 - '@rollup/rollup-darwin-arm64': 4.4.1 - '@rollup/rollup-darwin-x64': 4.4.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.4.1 - '@rollup/rollup-linux-arm64-gnu': 4.4.1 - '@rollup/rollup-linux-arm64-musl': 4.4.1 - '@rollup/rollup-linux-x64-gnu': 4.4.1 - '@rollup/rollup-linux-x64-musl': 4.4.1 - '@rollup/rollup-win32-arm64-msvc': 4.4.1 - '@rollup/rollup-win32-ia32-msvc': 4.4.1 - '@rollup/rollup-win32-x64-msvc': 4.4.1 - fsevents: 2.3.3 - dev: true - /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -5007,6 +5952,7 @@ packages: /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + dev: true /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} @@ -5014,6 +5960,7 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 + dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -5048,10 +5995,6 @@ packages: - supports-color dev: false - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: false - /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} engines: {node: '>= 0.4'} @@ -5109,13 +6052,9 @@ packages: get-intrinsic: 1.2.2 object-inspect: 1.13.1 - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true - /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false + dev: true /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -5128,14 +6067,20 @@ packages: is-arrayish: 0.3.2 dev: false + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} dev: true - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 dev: true /source-map-support@0.5.21: @@ -5177,12 +6122,19 @@ packages: engines: {node: '>= 10.x'} dev: false + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + /stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} dev: false - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 dev: true /standard-as-callback@2.1.0: @@ -5194,15 +6146,19 @@ packages: engines: {node: '>= 0.8'} dev: false - /std-env@3.5.0: - resolution: {integrity: sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==} - dev: true - /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} dev: false + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: true + /string-similarity@4.0.4: resolution: {integrity: sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -5215,6 +6171,7 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + dev: true /string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} @@ -5276,6 +6233,7 @@ packages: engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 + dev: true /strip-ansi@7.1.0: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} @@ -5289,9 +6247,14 @@ packages: engines: {node: '>=4'} dev: true - /strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} dev: true /strip-json-comments@3.1.1: @@ -5299,12 +6262,6 @@ packages: engines: {node: '>=8'} dev: true - /strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} - dependencies: - acorn: 8.11.2 - dev: true - /strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} dev: false @@ -5351,12 +6308,19 @@ packages: has-flag: 4.0.0 dev: true + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - /swagger-ui-dist@5.10.0: - resolution: {integrity: sha512-PBTn5qDOQVtU29hrx74km86SnK3/mFtF3grI98y575y1aRpxiuStRTIvsfXFudPFkLofHU7H9a+fKrP+Oayc3g==} + /swagger-ui-dist@5.10.5: + resolution: {integrity: sha512-Uv8E7hV/nXALQKgW86X1i58gl1O6DFg+Uq54sDwhYqucBBxj/47dLNw872TNILNlOTuPA6dRvUMGQdmlpaX8qQ==} dev: false /swagger-ui-express@5.0.0(express@4.18.2): @@ -5366,19 +6330,7 @@ packages: express: '>=4.0.0 || >=5.0.0-beta' dependencies: express: 4.18.2 - swagger-ui-dist: 5.10.0 - dev: false - - /tar@6.2.0: - resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} - engines: {node: '>=10'} - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 + swagger-ui-dist: 5.10.5 dev: false /tarn@3.0.2: @@ -5386,6 +6338,15 @@ packages: engines: {node: '>=8.0.0'} dev: false + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} dev: false @@ -5399,18 +6360,13 @@ packages: engines: {node: '>=8'} dev: false - /tinybench@2.5.1: - resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true - /tinypool@0.7.0: - resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} - engines: {node: '>=14.0.0'} - dev: true - - /tinyspy@2.2.0: - resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} - engines: {node: '>=14.0.0'} + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} dev: true /to-regex-range@5.0.1: @@ -5425,50 +6381,18 @@ packages: engines: {node: '>=0.6'} dev: false - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - - /tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - dev: true - /triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} dev: false - /ts-node-dev@2.0.0(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6): - resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} - engines: {node: '>=0.8.0'} - hasBin: true - peerDependencies: - node-notifier: '*' - typescript: '*' - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - chokidar: 3.5.3 - dynamic-dedupe: 0.3.0 - minimist: 1.2.8 - mkdirp: 1.0.4 - resolve: 1.22.8 - rimraf: 2.7.1 - source-map-support: 0.5.21 - tree-kill: 1.2.2 - ts-node: 10.9.1(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6) - tsconfig: 7.0.0 - typescript: 5.1.6 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@types/node' - dev: true + /ts-japi@1.9.1: + resolution: {integrity: sha512-//P5gsnmMmd0xGfRtTvltNHW917pv+z2YbvqqW6qEOKdjkmEHRHvU7TPtUqiriyhjkOMsPEMqsL3IoY0AlURyQ==} + engines: {node: '>=10'} + dev: false - /ts-node@10.9.1(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6): - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + /ts-node@10.9.2(@swc/core@1.3.96)(@types/node@18.17.5)(typescript@5.1.6): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: '@swc/core': '>=1.2.50' @@ -5508,15 +6432,6 @@ packages: strip-bom: 3.0.0 dev: true - /tsconfig@7.0.0: - resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} - dependencies: - '@types/strip-bom': 3.0.0 - '@types/strip-json-comments': 0.0.30 - strip-bom: 3.0.0 - strip-json-comments: 2.0.1 - dev: true - /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -5550,6 +6465,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -5606,10 +6526,6 @@ packages: hasBin: true dev: true - /ufo@1.3.2: - resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} - dev: true - /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -5624,6 +6540,17 @@ packages: engines: {node: '>= 0.8'} dev: false + /update-browserslist-db@1.0.13(browserslist@4.22.2): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.2 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: @@ -5647,6 +6574,15 @@ packages: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.21 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -5659,140 +6595,12 @@ packages: engines: {node: '>= 0.8'} dev: false - /vite-node@0.34.6(@types/node@18.17.5): - resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} - engines: {node: '>=v14.18.0'} - hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.3.4 - mlly: 1.4.2 - pathe: 1.1.1 - picocolors: 1.0.0 - vite: 5.0.0(@types/node@18.17.5) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true - - /vite@5.0.0(@types/node@18.17.5): - resolution: {integrity: sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 18.17.5 - esbuild: 0.19.5 - postcss: 8.4.31 - rollup: 4.4.1 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vitest@0.34.6: - resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} - engines: {node: '>=v14.18.0'} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' - happy-dom: '*' - jsdom: '*' - playwright: '*' - safaridriver: '*' - webdriverio: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: - '@types/chai': 4.3.10 - '@types/chai-subset': 1.3.5 - '@types/node': 18.17.5 - '@vitest/expect': 0.34.6 - '@vitest/runner': 0.34.6 - '@vitest/snapshot': 0.34.6 - '@vitest/spy': 0.34.6 - '@vitest/utils': 0.34.6 - acorn: 8.11.2 - acorn-walk: 8.3.0 - cac: 6.7.14 - chai: 4.3.10 - debug: 4.3.4 - local-pkg: 0.4.3 - magic-string: 0.30.5 - pathe: 1.1.1 - picocolors: 1.0.0 - std-env: 3.5.0 - strip-literal: 1.3.0 - tinybench: 2.5.1 - tinypool: 0.7.0 - vite: 5.0.0(@types/node@18.17.5) - vite-node: 0.34.6(@types/node@18.17.5) - why-is-node-running: 2.2.2 - transitivePeerDependencies: - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser + makeerror: 1.0.12 dev: true - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false - - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false - /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -5829,21 +6637,6 @@ packages: isexe: 2.0.0 dev: true - /why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - dev: true - - /wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - dependencies: - string-width: 4.2.3 - dev: false - /winston-transport@4.6.0: resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==} engines: {node: '>= 12.0.0'} @@ -5890,19 +6683,57 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true - /yaml@2.3.1: - resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} dev: false + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -5912,8 +6743,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - - /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - dev: true diff --git a/backend/node/pnpm-workspace.yaml b/backend/node/pnpm-workspace.yaml deleted file mode 100644 index 1bbcf3c..0000000 --- a/backend/node/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - - 'src/infrastructure/server/*' diff --git a/backend/node/src/adapters/controllers/create-user.controller.spec.ts b/backend/node/src/adapters/controllers/create-user.controller.spec.ts index 328717f..e85cfe0 100644 --- a/backend/node/src/adapters/controllers/create-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/create-user.controller.spec.ts @@ -1,4 +1,5 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; +import { mockedCreateUser } from '../../../__tests__/mocks/user.mock'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { CreateUserDto, @@ -6,23 +7,19 @@ import type { UserResult, UserTable, } from '../../core/interfaces/user.interface'; -import { createUser } from '../../core/use-cases/create-user.use-case'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequestBody } from '../interfaces/http.interface'; -import type { UserResponse } from '../interfaces/user.interface'; import { createUserController } from './create-user.controller'; -vi.mock('../../core/use-cases/create-user.use-case'); - describe('createUserController', () => { const userRepository: UserRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; const dto: CreateUserDto = { @@ -45,8 +42,6 @@ describe('createUserController', () => { updated_at: '2022-06-11 01:55:13', }; - const mockedCreateUser = vi.mocked(createUser, true); - beforeEach(() => { mockedCreateUser.mockImplementation(() => { return Promise.resolve({ @@ -67,17 +62,19 @@ describe('createUserController', () => { }); test('should create an user', async () => { - request = { ...request, body: dto }; + request = { + ...request, + body: dto, + }; const data = await controller(request); - expect(data).toEqual>({ - status: 201, + expect>(data).toEqual({ + status: StatusCodes.CREATED, data: { ...user, photo: null, avatar: null, - type: 'users', }, }); }); diff --git a/backend/node/src/adapters/controllers/create-user.controller.ts b/backend/node/src/adapters/controllers/create-user.controller.ts index 7186533..6a48801 100644 --- a/backend/node/src/adapters/controllers/create-user.controller.ts +++ b/backend/node/src/adapters/controllers/create-user.controller.ts @@ -1,21 +1,22 @@ +import { StatusCodes } from 'http-status-codes'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { CreateUserDto, UserRepository, + UserResult, } from '../../core/interfaces/user.interface'; import { createUser } from '../../core/use-cases/create-user.use-case'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequestBody } from '../interfaces/http.interface'; -import type { UserResponse } from '../interfaces/user.interface'; export function createUserController( userRepository: UserRepository ): ( req: HttpRequestBody> -) => Promise> { +) => Promise> { return async ( req: HttpRequestBody> - ): Promise> => { + ): Promise> => { if (!req.body) { throw new BadRequestException('Request body is empty.'); } @@ -28,11 +29,8 @@ export function createUserController( const data = await createUser(dto, userRepository); return { - status: 201, - data: { - ...data, - type: 'users', - }, + status: StatusCodes.CREATED, + data, }; }; } diff --git a/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts b/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts index 459aa96..9fc08c3 100644 --- a/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts +++ b/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts @@ -1,31 +1,28 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; +import { mockedFindAllUsers } from '../../../__tests__/mocks/user.mock'; import type { FileService } from '../../core/interfaces/file.interface'; import type { UserRepository, UserResult, UserTable, } from '../../core/interfaces/user.interface'; -import { findAllUsers } from '../../core/use-cases/find-all-users.use-case'; import type { HttpRequest } from '../interfaces/http.interface'; -import type { UserResponse } from '../interfaces/user.interface'; import { findAllUsersController } from './find-all-users.controller'; -vi.mock('../../core/use-cases/find-all-users.use-case'); - describe('findAllUsersController', () => { const userRepository: UserRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; const fileService: FileService = { - upload: vi.fn(), - getUrl: vi.fn(), - remove: vi.fn(), + upload: jest.fn(), + getUrl: jest.fn(), + remove: jest.fn(), }; const controller = findAllUsersController(userRepository, fileService); @@ -48,8 +45,6 @@ describe('findAllUsersController', () => { updated_at: '2022-06-11 01:55:13', }; - const mockedFindAllUsers = vi.mocked(findAllUsers, true); - beforeEach(() => { mockedFindAllUsers.mockImplementation(() => { return Promise.resolve([ @@ -66,36 +61,37 @@ describe('findAllUsersController', () => { const data = await controller(request); - expect(data.status).toEqual(200); + expect(data.status).toEqual(StatusCodes.OK); expect(data.data).toEqual([]); }); test('should return all users', async () => { const data = await controller(request); - expect(data.status).toEqual(200); + expect(data.status).toEqual(StatusCodes.OK); expect(data.data).toEqual( - expect.arrayContaining([ + expect.arrayContaining([ { ...user, avatar: null, - type: 'users', }, ]) ); }); test('should return all users with avatar', async () => { - user = { ...user, photo: 'photo.png' }; + user = { + ...user, + photo: 'photo.png', + }; const data = await controller(request); - expect(data.status).toEqual(200); + expect(data.status).toEqual(StatusCodes.OK); expect(data.data).toEqual( - expect.arrayContaining([ + expect.arrayContaining([ { ...user, avatar: 'avatar.png', - type: 'users', }, ]) ); diff --git a/backend/node/src/adapters/controllers/find-all-users.controller.ts b/backend/node/src/adapters/controllers/find-all-users.controller.ts index d88bb18..a5108b7 100644 --- a/backend/node/src/adapters/controllers/find-all-users.controller.ts +++ b/backend/node/src/adapters/controllers/find-all-users.controller.ts @@ -1,25 +1,23 @@ +import { StatusCodes } from 'http-status-codes'; import type { FileService } from '../../core/interfaces/file.interface'; -import type { UserRepository } from '../../core/interfaces/user.interface'; +import type { + UserRepository, + UserResult, +} from '../../core/interfaces/user.interface'; import { findAllUsers } from '../../core/use-cases/find-all-users.use-case'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequest } from '../interfaces/http.interface'; -import type { UserResponse } from '../interfaces/user.interface'; export function findAllUsersController( userRepository: UserRepository, fileService: FileService -): (req: HttpRequest) => Promise> { - return async (_req: HttpRequest): Promise> => { +): (req: HttpRequest) => Promise> { + return async (_req: HttpRequest): Promise> => { const data = await findAllUsers(userRepository, fileService); return { - status: 200, - data: data?.map((obj) => { - return { - ...obj, - type: 'users', - }; - }), + status: StatusCodes.OK, + data, }; }; } diff --git a/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts b/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts index 719cd5d..da429f6 100644 --- a/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts @@ -1,4 +1,5 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; +import { mockedFindOneUser } from '../../../__tests__/mocks/user.mock'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; import type { @@ -6,31 +7,25 @@ import type { UserResult, UserTable, } from '../../core/interfaces/user.interface'; -import { findOneUser } from '../../core/use-cases/find-one-user.use-case'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequestParams } from '../interfaces/http.interface'; -import type { - UserRequestParams, - UserResponse, -} from '../interfaces/user.interface'; +import type { UserRequestParams } from '../interfaces/user.interface'; import { findOneUserController } from './find-one-user.controller'; -vi.mock('../../core/use-cases/find-one-user.use-case'); - describe('findOneUserController', () => { const userRepository: UserRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; const fileService: FileService = { - upload: vi.fn(), - getUrl: vi.fn(), - remove: vi.fn(), + upload: jest.fn(), + getUrl: jest.fn(), + remove: jest.fn(), }; const controller = findOneUserController(userRepository, fileService); @@ -47,8 +42,6 @@ describe('findOneUserController', () => { updated_at: '2022-06-11 01:55:13', }; - const mockedFindOneUser = vi.mocked(findOneUser, true); - beforeEach(() => { mockedFindOneUser.mockImplementation(() => { return Promise.resolve({ @@ -65,31 +58,37 @@ describe('findOneUserController', () => { }); test('should return an user', async () => { - request = { ...request, params: { id: 'id' } }; + request = { + ...request, + params: { + id: 'id', + }, + }; const data = await controller(request); - expect(data).toEqual>({ - status: 200, + expect>(data).toEqual({ + status: StatusCodes.OK, data: { ...user, avatar: null, - type: 'users', }, }); }); test('should return an user with avatar', async () => { - user = { ...user, photo: 'photo.png' }; + user = { + ...user, + photo: 'photo.png', + }; const data = await controller(request); - expect(data).toEqual>({ - status: 200, + expect>(data).toEqual({ + status: StatusCodes.OK, data: { ...user, avatar: 'avatar.png', - type: 'users', }, }); }); diff --git a/backend/node/src/adapters/controllers/find-one-user.controller.ts b/backend/node/src/adapters/controllers/find-one-user.controller.ts index 66eb00c..c3eafbd 100644 --- a/backend/node/src/adapters/controllers/find-one-user.controller.ts +++ b/backend/node/src/adapters/controllers/find-one-user.controller.ts @@ -1,23 +1,24 @@ +import { StatusCodes } from 'http-status-codes'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; -import type { UserRepository } from '../../core/interfaces/user.interface'; +import type { + UserRepository, + UserResult, +} from '../../core/interfaces/user.interface'; import { findOneUser } from '../../core/use-cases/find-one-user.use-case'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequestParams } from '../interfaces/http.interface'; -import type { - UserRequestParams, - UserResponse, -} from '../interfaces/user.interface'; +import type { UserRequestParams } from '../interfaces/user.interface'; export function findOneUserController( userRepository: UserRepository, fileService: FileService ): ( req: HttpRequestParams -) => Promise> { +) => Promise> { return async ( req: HttpRequestParams - ): Promise> => { + ): Promise> => { if (!req.params) { throw new BadRequestException('An id parameter is expexted.'); } @@ -27,8 +28,8 @@ export function findOneUserController( const data = await findOneUser(id, userRepository, fileService); return { - status: 200, - data: { ...data, type: 'users' }, + status: StatusCodes.OK, + data, }; }; } diff --git a/backend/node/src/adapters/controllers/home.controller.spec.ts b/backend/node/src/adapters/controllers/home.controller.spec.ts index ff42ef3..f725b0c 100644 --- a/backend/node/src/adapters/controllers/home.controller.spec.ts +++ b/backend/node/src/adapters/controllers/home.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequest } from '../interfaces/http.interface'; import { homeController } from './home.controller'; @@ -9,8 +9,8 @@ describe('homeController', () => { test('should return hello world!', () => { const data = homeController(request); - expect(data).toEqual>({ - status: 200, + expect>(data).toEqual({ + status: StatusCodes.OK, data: 'Hello world!', }); }); diff --git a/backend/node/src/adapters/controllers/home.controller.ts b/backend/node/src/adapters/controllers/home.controller.ts index 33ee02f..ac07c4e 100644 --- a/backend/node/src/adapters/controllers/home.controller.ts +++ b/backend/node/src/adapters/controllers/home.controller.ts @@ -1,9 +1,10 @@ +import { StatusCodes } from 'http-status-codes'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequest } from '../interfaces/http.interface'; export function homeController(_req: HttpRequest): ResponseModel { return { - status: 200, + status: StatusCodes.OK, data: 'Hello world!', }; } diff --git a/backend/node/src/adapters/controllers/remove-user.controller.spec.ts b/backend/node/src/adapters/controllers/remove-user.controller.spec.ts index 81dbd9c..fc76aba 100644 --- a/backend/node/src/adapters/controllers/remove-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/remove-user.controller.spec.ts @@ -1,4 +1,5 @@ -import { beforeAll, describe, expect, test, vi } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; +import { mockedRemoveUser } from '../../../__tests__/mocks/user.mock'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; import type { UserRepository } from '../../core/interfaces/user.interface'; @@ -8,30 +9,26 @@ import type { HttpRequestParams } from '../interfaces/http.interface'; import type { UserRequestParams } from '../interfaces/user.interface'; import { removeUserController } from './remove-user.controller'; -vi.mock('../../core/use-cases/remove-user.use-case'); - describe('removeUserController', () => { const userRepository: UserRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; const fileService: FileService = { - upload: vi.fn(), - getUrl: vi.fn(), - remove: vi.fn(), + upload: jest.fn(), + getUrl: jest.fn(), + remove: jest.fn(), }; let request: HttpRequestParams = {}; const controller = removeUserController(userRepository, fileService); - const mockedRemoveUser = vi.mocked(removeUser, true); - beforeAll(() => { mockedRemoveUser.mockReturnValue(Promise.resolve()); }); @@ -43,12 +40,21 @@ describe('removeUserController', () => { }); test('should remove an user', async () => { - request = { ...request, params: { id: 'id' } }; + mockedRemoveUser.mockReturnValue(Promise.resolve()); + + request = { + ...request, + params: { + id: 'id', + }, + }; const data = await controller(request); expect(removeUser).toBeCalledTimes(1); expect(removeUser).toBeCalledWith('id', userRepository, fileService); - expect(data).toEqual({ status: 204 }); + expect(data).toEqual({ + status: StatusCodes.NO_CONTENT, + }); }); }); diff --git a/backend/node/src/adapters/controllers/remove-user.controller.ts b/backend/node/src/adapters/controllers/remove-user.controller.ts index a84f401..794cb91 100644 --- a/backend/node/src/adapters/controllers/remove-user.controller.ts +++ b/backend/node/src/adapters/controllers/remove-user.controller.ts @@ -1,3 +1,4 @@ +import { StatusCodes } from 'http-status-codes'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; import type { UserRepository } from '../../core/interfaces/user.interface'; @@ -21,6 +22,8 @@ export function removeUserController( await removeUser(id, userRepository, fileService); - return { status: 204 }; + return { + status: StatusCodes.NO_CONTENT, + }; }; } diff --git a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts index 26888ae..de3a593 100644 --- a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts +++ b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts @@ -1,28 +1,27 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { mockedUniqueUserEmail } from '../../../__tests__/mocks/user.mock'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { UpdateUserDto, UserRepository, UserTable, } from '../../core/interfaces/user.interface'; -import { uniqueUserEmail } from '../../core/use-cases/unique-user-email.use-case'; -import type { HttpRequestBody } from '../interfaces/http.interface'; +import { HttpRequest } from '../interfaces/http.interface'; import { uniqueUserEmailController } from './unique-user-email.controller'; -vi.mock('../../core/use-cases/unique-user-email.use-case'); - describe('uniqueUserEmailController', () => { const userRepository: UserRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; - let dto: Pick = { email: 'johndoe@example.com' }; + let dto: Pick = { + email: 'johndoe@example.com', + }; const user: UserTable = { id: 'id', @@ -34,20 +33,24 @@ describe('uniqueUserEmailController', () => { updated_at: '2022-06-11 01:55:13', }; - let request: HttpRequestBody> = {}; + let request: HttpRequest< + unknown, + Pick, + Pick + > = {}; const controller = uniqueUserEmailController(userRepository); - const mockedUniqueUserEmail = vi.mocked(uniqueUserEmail, true); - beforeEach(() => { - mockedUniqueUserEmail.mockImplementation((email: string) => { - if (email === user.email) { - throw new BadRequestException('The email has already been taken.'); - } + mockedUniqueUserEmail.mockImplementation( + (_repo: UserRepository, email: string) => { + if (email === user.email) { + throw new BadRequestException('The email has already been taken.'); + } - return Promise.resolve(true); - }); + return Promise.resolve(true); + } + ); }); test('should only check when email is provided', async () => { @@ -65,8 +68,14 @@ describe('uniqueUserEmailController', () => { }); test('should return true when email is unique', async () => { - dto = { ...dto, email: 'jandoe@example.com' }; - request = { ...request, body: dto }; + dto = { + ...dto, + email: 'jandoe@example.com', + }; + request = { + ...request, + body: dto, + }; const data = await controller(request); diff --git a/backend/node/src/adapters/controllers/unique-user-email.controller.ts b/backend/node/src/adapters/controllers/unique-user-email.controller.ts index c4ecbb4..a7665ae 100644 --- a/backend/node/src/adapters/controllers/unique-user-email.controller.ts +++ b/backend/node/src/adapters/controllers/unique-user-email.controller.ts @@ -1,26 +1,35 @@ -import { uniqueUserEmail } from '../../core/use-cases/unique-user-email.use-case'; import type { UpdateUserDto, UserRepository, + UserTable, } from '../../core/interfaces/user.interface'; -import type { HttpRequestBody } from '../interfaces/http.interface'; +import { uniqueUserEmail } from '../../core/use-cases/unique-user-email.use-case'; +import type { HttpRequest } from '../interfaces/http.interface'; export function uniqueUserEmailController( userRepository: UserRepository -): (req: HttpRequestBody>) => Promise { +): ( + req: HttpRequest, Pick> +) => Promise { return async ( - req: HttpRequestBody> + req: HttpRequest< + unknown, + Pick, + Pick + > ): Promise => { - if (req.body) { - const { email } = req.body; + if (!req.body) { + return true; + } - if (email) { - const result = await uniqueUserEmail(email, userRepository); + const { email } = req.body; - return result; - } + if (!email) { + return true; } - return true; + const result = await uniqueUserEmail(userRepository, email, req.params?.id); + + return result; }; } diff --git a/backend/node/src/adapters/controllers/update-user.controller.spec.ts b/backend/node/src/adapters/controllers/update-user.controller.spec.ts index 2840c6e..8bfd8d1 100644 --- a/backend/node/src/adapters/controllers/update-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/update-user.controller.spec.ts @@ -1,4 +1,5 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; +import { mockedUpdateUser } from '../../../__tests__/mocks/user.mock'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; import type { @@ -6,27 +7,24 @@ import type { UserRepository, UserTable, } from '../../core/interfaces/user.interface'; -import { updateUser } from '../../core/use-cases/update-user.use-case'; import type { HttpRequest } from '../interfaces/http.interface'; import type { UserRequestParams } from '../interfaces/user.interface'; import { updateUserController } from './update-user.controller'; -vi.mock('../../core/use-cases/update-user.use-case'); - describe('updateUserController', () => { const userRepository: UserRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; const fileService: FileService = { - upload: vi.fn(), - getUrl: vi.fn(), - remove: vi.fn(), + upload: jest.fn(), + getUrl: jest.fn(), + remove: jest.fn(), }; let dto: UpdateUserDto = {}; @@ -51,8 +49,6 @@ describe('updateUserController', () => { const controller = updateUserController(userRepository, fileService); - const mockedUpdateUser = vi.mocked(updateUser, true); - beforeEach(() => { mockedUpdateUser.mockImplementation(() => { return Promise.resolve({ @@ -78,11 +74,10 @@ describe('updateUserController', () => { const data = await controller(request); expect(data).toEqual({ - status: 200, + status: StatusCodes.OK, data: { ...user, avatar: null, - type: 'users', }, }); }); @@ -96,13 +91,12 @@ describe('updateUserController', () => { const data = await controller(request); expect(data).toEqual({ - status: 200, + status: StatusCodes.OK, data: { ...user, first_name: 'John', last_name: 'Doe', avatar: null, - type: 'users', }, }); }); @@ -112,12 +106,11 @@ describe('updateUserController', () => { const data = await controller(request); expect(data).toEqual({ - status: 200, + status: StatusCodes.OK, data: { ...user, email: 'janedoe@example.com', avatar: null, - type: 'users', }, }); }); @@ -129,11 +122,10 @@ describe('updateUserController', () => { const data = await controller(request); expect(data).toEqual({ - status: 200, + status: StatusCodes.OK, data: { ...user, avatar: 'avatar.png', - type: 'users', }, }); }); diff --git a/backend/node/src/adapters/controllers/update-user.controller.ts b/backend/node/src/adapters/controllers/update-user.controller.ts index 27624fc..65a176a 100644 --- a/backend/node/src/adapters/controllers/update-user.controller.ts +++ b/backend/node/src/adapters/controllers/update-user.controller.ts @@ -1,26 +1,25 @@ +import { StatusCodes } from 'http-status-codes'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { FileService } from '../../core/interfaces/file.interface'; import type { UpdateUserDto, UserRepository, + UserResult, } from '../../core/interfaces/user.interface'; import { updateUser } from '../../core/use-cases/update-user.use-case'; import type { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequest } from '../interfaces/http.interface'; -import type { - UserRequestParams, - UserResponse, -} from '../interfaces/user.interface'; +import type { UserRequestParams } from '../interfaces/user.interface'; export function updateUserController( userRepository: UserRepository, fileService: FileService ): ( req: HttpRequest> -) => Promise> { +) => Promise> { return async ( req: HttpRequest> - ): Promise> => { + ): Promise> => { if (!req.params) { throw new BadRequestException('An id parameter is expected.'); } @@ -32,8 +31,8 @@ export function updateUserController( const data = await updateUser(id, dto, userRepository, fileService); return { - status: 200, - data: { ...data, type: 'users' }, + status: StatusCodes.OK, + data, }; }; } diff --git a/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts b/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts index 09b10cf..eee9c6b 100644 --- a/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts +++ b/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts @@ -1,4 +1,3 @@ -import { describe, expect, test } from 'vitest'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { RequestIdParams } from '../interfaces/common.interface'; import type { HttpRequestParams } from '../interfaces/http.interface'; @@ -14,7 +13,12 @@ describe('validateUuidController', () => { }); test('should throw error when id is invalid uuid', () => { - request = { ...request, params: { id: 'id' } }; + request = { + ...request, + params: { + id: 'id', + }, + }; expect(() => { validateUuidController(request); @@ -26,7 +30,9 @@ describe('validateUuidController', () => { test('should return true when id is valid uuid', () => { request = { ...request, - params: { id: 'a2e4b207-be4d-45ab-81a1-bcdf565cc9be' }, + params: { + id: 'a2e4b207-be4d-45ab-81a1-bcdf565cc9be', + }, }; const data = validateUuidController(request); diff --git a/backend/node/src/adapters/interfaces/http.interface.ts b/backend/node/src/adapters/interfaces/http.interface.ts index 00c3542..bbe7420 100644 --- a/backend/node/src/adapters/interfaces/http.interface.ts +++ b/backend/node/src/adapters/interfaces/http.interface.ts @@ -50,11 +50,7 @@ export interface JsonApiDataObject> { export interface JsonApiData> extends JsonApi { - data: - | JsonApiDataObject - | JsonApiDataObject[] - | [] - | null; + data: JsonApiDataObject; } export interface JsonApiErrorObject { diff --git a/backend/node/src/adapters/interfaces/user.interface.ts b/backend/node/src/adapters/interfaces/user.interface.ts index 98bf69b..6aabb09 100644 --- a/backend/node/src/adapters/interfaces/user.interface.ts +++ b/backend/node/src/adapters/interfaces/user.interface.ts @@ -15,4 +15,4 @@ export interface UserResponse extends UserResult { type: 'users'; } -export type UserData = Omit; +export type UserData = Omit; diff --git a/backend/node/src/core/exceptions/bad-request.exception.ts b/backend/node/src/core/exceptions/bad-request.exception.ts index dd50a41..30d902a 100644 --- a/backend/node/src/core/exceptions/bad-request.exception.ts +++ b/backend/node/src/core/exceptions/bad-request.exception.ts @@ -1,3 +1,4 @@ +import { ReasonPhrases, StatusCodes } from 'http-status-codes'; import type { ErrorObject } from '../interfaces/http.interface'; import { HttpException } from './http.exception'; @@ -5,7 +6,7 @@ export class BadRequestException extends HttpException { declare readonly error?: ErrorObject | Record; constructor(message: string, error?: ErrorObject) { - super(400, message, 'Bad Request'); + super(StatusCodes.BAD_REQUEST, message, ReasonPhrases.BAD_REQUEST); if (error && Object.keys(error).length) { this.error = error; diff --git a/backend/node/src/core/exceptions/not-found.exception.ts b/backend/node/src/core/exceptions/not-found.exception.ts index 19063a9..3ad21b2 100644 --- a/backend/node/src/core/exceptions/not-found.exception.ts +++ b/backend/node/src/core/exceptions/not-found.exception.ts @@ -1,7 +1,8 @@ +import { ReasonPhrases, StatusCodes } from 'http-status-codes'; import { HttpException } from './http.exception'; export class NotFoundException extends HttpException { constructor(message: string) { - super(404, message, 'Not Found'); + super(StatusCodes.NOT_FOUND, message, ReasonPhrases.NOT_FOUND); } } diff --git a/backend/node/src/core/interfaces/common.interface.ts b/backend/node/src/core/interfaces/common.interface.ts index 706c735..c1c4032 100644 --- a/backend/node/src/core/interfaces/common.interface.ts +++ b/backend/node/src/core/interfaces/common.interface.ts @@ -1,6 +1,6 @@ export interface BaseRepository { create: (data: TableInput) => Promise; - findAll: () => Promise; + findAll: () => Promise; findOne: (id: string) => Promise
; update: (id: string, data: Partial) => Promise
; remove: (id: string) => Promise
; diff --git a/backend/node/src/core/interfaces/file.interface.ts b/backend/node/src/core/interfaces/file.interface.ts index 0b830c9..274501d 100644 --- a/backend/node/src/core/interfaces/file.interface.ts +++ b/backend/node/src/core/interfaces/file.interface.ts @@ -2,6 +2,6 @@ import type { File } from '../entities/common.entity'; export interface FileService { upload: (file?: File, originalName?: string | null) => Promise; - getUrl: (path: string) => Promise; + getUrl: (path: string) => Promise; remove: (path: string) => Promise; } diff --git a/backend/node/src/core/interfaces/user.interface.ts b/backend/node/src/core/interfaces/user.interface.ts index bb53923..73b4518 100644 --- a/backend/node/src/core/interfaces/user.interface.ts +++ b/backend/node/src/core/interfaces/user.interface.ts @@ -3,6 +3,7 @@ import type { BaseRepository } from './common.interface'; export interface CreateUserDto extends Omit { + id?: string; last_name: string | null; nickname?: string; } @@ -18,15 +19,15 @@ export interface UserTable updated_at: string; } -export type UserTableInput = Omit< - UserTable, - 'id' | 'created_at' | 'updated_at' -> & - Partial>; +export interface UserTableInput + extends Omit, + Partial> { + id?: string; +} export interface UserRepository extends BaseRepository { - findOneByEmail: (email: string) => Promise; + findOneByEmail: (email: string, id?: string) => Promise; } export type UserResult = UserTable & Pick; diff --git a/backend/node/src/core/use-cases/create-user.use-case.spec.ts b/backend/node/src/core/use-cases/create-user.use-case.spec.ts index 7e5efa9..8649331 100644 --- a/backend/node/src/core/use-cases/create-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/create-user.use-case.spec.ts @@ -1,4 +1,3 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { CreateUserDto, UserRepository, @@ -23,30 +22,29 @@ describe('createUser', () => { last_name: dto.last_name, nickname: null, email: dto.email, - email_verified_at: null, created_at: '2022-06-11 01:55:13', updated_at: '2022-06-11 01:55:13', }; beforeEach(() => { userRepository = { - create: vi.fn((data: UserTableInput) => { + create: jest.fn((data: UserTableInput) => { return Promise.resolve({ ...user, ...data, }); }), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; }); test('should throw error when user is not saved', async () => { - userRepository.create = vi.fn().mockReturnValue(Promise.resolve(null)); + userRepository.create = jest.fn().mockReturnValue(Promise.resolve(null)); await expect(createUser(dto, userRepository)).rejects.toThrow( new Error('Something went wrong.') @@ -57,7 +55,7 @@ describe('createUser', () => { const data = await createUser(dto, userRepository); expect(userRepository.create).toBeCalledTimes(1); - expect(data).toEqual({ + expect(data).toEqual({ ...user, avatar: null, }); diff --git a/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts b/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts index 243b9a9..087ded8 100644 --- a/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts +++ b/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts @@ -1,10 +1,5 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { FileService } from '../interfaces/file.interface'; -import type { - UserRepository, - UserResult, - UserTable, -} from '../interfaces/user.interface'; +import type { UserRepository, UserTable } from '../interfaces/user.interface'; import { findAllUsers } from './find-all-users.use-case'; describe('findAllUsers', () => { @@ -23,24 +18,24 @@ describe('findAllUsers', () => { beforeEach(() => { userRepository = { - create: vi.fn(), - findAll: vi.fn().mockReturnValue(Promise.resolve([user])), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + create: jest.fn(), + findAll: jest.fn().mockReturnValue(Promise.resolve([user])), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; fileService = { - upload: vi.fn(), - getUrl: vi.fn().mockReturnValue(Promise.resolve('avatar.png')), - remove: vi.fn(), + upload: jest.fn(), + getUrl: jest.fn().mockReturnValue(Promise.resolve('avatar.png')), + remove: jest.fn(), }; }); test('should return empty array', async () => { - userRepository.findAll = vi.fn().mockReturnValue(Promise.resolve([])); + userRepository.findAll = jest.fn().mockReturnValue(Promise.resolve([])); const data = await findAllUsers(userRepository, fileService); @@ -54,7 +49,7 @@ describe('findAllUsers', () => { expect(userRepository.findAll).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(0); expect(data).toEqual( - expect.arrayContaining([ + expect.arrayContaining([ { ...user, avatar: null, @@ -64,7 +59,7 @@ describe('findAllUsers', () => { }); test('should return all users with avatar', async () => { - userRepository.findAll = vi + userRepository.findAll = jest .fn() .mockReturnValue(Promise.resolve([{ ...user, photo: 'photo.png' }])); @@ -72,7 +67,7 @@ describe('findAllUsers', () => { expect(userRepository.findAll).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(1); expect(data).toEqual( - expect.arrayContaining([ + expect.arrayContaining([ { ...user, photo: 'photo.png', diff --git a/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts b/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts index cd92b0b..19bef86 100644 --- a/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts @@ -1,4 +1,3 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { @@ -24,25 +23,25 @@ describe('findOneUser', () => { beforeEach(() => { userRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn((id: string) => { + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn((id: string) => { if (id !== user.id) { return Promise.resolve(null); } return Promise.resolve(user); }), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; fileService = { - upload: vi.fn(), - getUrl: vi.fn().mockReturnValue(Promise.resolve('avatar.png')), - remove: vi.fn(), + upload: jest.fn(), + getUrl: jest.fn().mockReturnValue(Promise.resolve('avatar.png')), + remove: jest.fn(), }; }); @@ -56,14 +55,14 @@ describe('findOneUser', () => { const data = await findOneUser(user.id, userRepository, fileService); expect(userRepository.findOne).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(0); - expect(data).toEqual({ + expect(data).toEqual({ ...user, avatar: null, }); }); test('should return an user with avatar', async () => { - userRepository.findOne = vi.fn().mockReturnValue( + userRepository.findOne = jest.fn().mockReturnValue( Promise.resolve({ ...user, photo: 'photo.png', @@ -73,7 +72,7 @@ describe('findOneUser', () => { const data = await findOneUser(user.id, userRepository, fileService); expect(userRepository.findOne).toBeCalledTimes(1); expect(fileService.getUrl).toBeCalledTimes(1); - expect(data).toEqual({ + expect(data).toEqual({ ...user, photo: 'photo.png', avatar: 'avatar.png', diff --git a/backend/node/src/core/use-cases/remove-user.use-case.spec.ts b/backend/node/src/core/use-cases/remove-user.use-case.spec.ts index 6eee756..ff20ac3 100644 --- a/backend/node/src/core/use-cases/remove-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/remove-user.use-case.spec.ts @@ -1,4 +1,3 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { UserRepository, UserTable } from '../interfaces/user.interface'; @@ -20,25 +19,25 @@ describe('removeUser', () => { beforeEach(() => { userRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn(), - remove: vi.fn((id: string) => { + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn(), + remove: jest.fn((id: string) => { if (id !== user.id) { return Promise.resolve(null); } return Promise.resolve(user); }), - truncate: vi.fn(), + truncate: jest.fn(), }; fileService = { - upload: vi.fn(), - getUrl: vi.fn(), - remove: vi.fn(), + upload: jest.fn(), + getUrl: jest.fn(), + remove: jest.fn(), }; }); @@ -56,7 +55,7 @@ describe('removeUser', () => { }); test('should delete user with an avatar', async () => { - userRepository.remove = vi.fn().mockReturnValue( + userRepository.remove = jest.fn().mockReturnValue( Promise.resolve({ ...user, photo: 'photo.png', diff --git a/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts b/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts index 54e2294..879f589 100644 --- a/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts +++ b/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts @@ -1,4 +1,3 @@ -import { beforeAll, describe, expect, test, vi } from 'vitest'; import { BadRequestException } from '../exceptions/bad-request.exception'; import type { UserRepository, UserTable } from '../interfaces/user.interface'; import { uniqueUserEmail } from './unique-user-email.use-case'; @@ -18,32 +17,32 @@ describe('uniqueUserEmail', () => { beforeAll(() => { userRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn((email: string) => { + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn((email: string) => { if (email === user.email) { return Promise.resolve(user); } return Promise.resolve(null); }), - update: vi.fn(), - remove: vi.fn(), - truncate: vi.fn(), + update: jest.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; }); test('should throw error when email is already taken', async () => { await expect( - uniqueUserEmail('johndoe@example.com', userRepository) + uniqueUserEmail(userRepository, 'johndoe@example.com') ).rejects.toThrow( new BadRequestException('The email has already been taken.') ); }); test('should return true when user is not found', async () => { - const data = await uniqueUserEmail('johndoe@example.co', userRepository); + const data = await uniqueUserEmail(userRepository, 'johndoe@example.co'); expect(data).toEqual(true); }); diff --git a/backend/node/src/core/use-cases/unique-user-email.use-case.ts b/backend/node/src/core/use-cases/unique-user-email.use-case.ts index c2bd0f0..36d6534 100644 --- a/backend/node/src/core/use-cases/unique-user-email.use-case.ts +++ b/backend/node/src/core/use-cases/unique-user-email.use-case.ts @@ -2,10 +2,11 @@ import { BadRequestException } from '../exceptions/bad-request.exception'; import type { UserRepository } from '../interfaces/user.interface'; export async function uniqueUserEmail( + userRepository: UserRepository, email: string, - userRepository: UserRepository + userId?: string ) { - const isExists = await userRepository.findOneByEmail(email); + const isExists = await userRepository.findOneByEmail(email, userId); if (isExists) { throw new BadRequestException('The email has already been taken.'); diff --git a/backend/node/src/core/use-cases/update-user.use-case.spec.ts b/backend/node/src/core/use-cases/update-user.use-case.spec.ts index ee33572..2c69b63 100644 --- a/backend/node/src/core/use-cases/update-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/update-user.use-case.spec.ts @@ -1,4 +1,3 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { @@ -29,31 +28,31 @@ describe('updateUser', () => { beforeEach(() => { userRepository = { - create: vi.fn(), - findAll: vi.fn(), - findOne: vi.fn(), - findOneByEmail: vi.fn(), - update: vi.fn((id: string, data: Partial) => { + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByEmail: jest.fn(), + update: jest.fn((id: string, data: Partial) => { if (id !== user.id) { return Promise.resolve(null); } return Promise.resolve({ ...user, ...data }); }), - remove: vi.fn(), - truncate: vi.fn(), + remove: jest.fn(), + truncate: jest.fn(), }; fileService = { - upload: vi.fn(), - getUrl: vi.fn().mockImplementation(() => { + upload: jest.fn(), + getUrl: jest.fn().mockImplementation((_path: string) => { if (!user.photo) { - return null; + return Promise.resolve(null); } return Promise.resolve('avatar.png'); }), - remove: vi.fn(), + remove: jest.fn(), }; }); @@ -71,7 +70,11 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ ...user, ...dto, avatar: null }); + expect(data).toEqual({ + ...user, + ...dto, + avatar: null, + }); }); test("should only update user's nickname", async () => { @@ -81,7 +84,11 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ ...user, ...dto, avatar: null }); + expect(data).toEqual({ + ...user, + ...dto, + avatar: null, + }); }); test("should only update user's email", async () => { @@ -91,7 +98,11 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ ...user, ...dto, avatar: null }); + expect(data).toEqual({ + ...user, + ...dto, + avatar: null, + }); }); test("should only update existing user's avatar", async () => { @@ -103,7 +114,7 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ + expect(data).toEqual({ ...user, photo: 'photo.png', avatar: 'avatar.png', diff --git a/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts b/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts index 667d3cd..80eebdb 100644 --- a/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts +++ b/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts @@ -1,4 +1,3 @@ -import { describe, expect, test } from 'vitest'; import { BadRequestException } from '../exceptions/bad-request.exception'; import { validateUuid } from './validate-uuid.use-case'; diff --git a/backend/node/src/infrastructure/config/app.ts b/backend/node/src/infrastructure/config/app.ts index d59e981..1a3c741 100644 --- a/backend/node/src/infrastructure/config/app.ts +++ b/backend/node/src/infrastructure/config/app.ts @@ -2,10 +2,12 @@ interface AppConfig { name: string; host: string; port: number; + url: string; } export const appConfig: AppConfig = { name: process.env.APP_NAME || 'express', host: process.env.APP_HOST || 'localhost', port: parseInt(String(process.env.APP_PORT), 10) || 3000, + url: process.env.APP_URL || 'http://localhost:3000', }; diff --git a/backend/node/src/infrastructure/repositories/user.repository.ts b/backend/node/src/infrastructure/repositories/user.repository.ts index 1a3be35..07808e7 100644 --- a/backend/node/src/infrastructure/repositories/user.repository.ts +++ b/backend/node/src/infrastructure/repositories/user.repository.ts @@ -41,7 +41,7 @@ export const userRepository: UserRepository = { 'updated_at' ); - const result: UserTable[] = records; + const result = records; return result; }, @@ -64,14 +64,20 @@ export const userRepository: UserRepository = { return null; } - const result: UserTable = record; + const result = record; return result; }, - findOneByEmail: async (email: string) => { + findOneByEmail: async (email: string, id?: string) => { const record = await db('users') .where('email', '=', email) + .modify((query) => { + if (id) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + query.andWhereNot('id', '=', id); + } + }) .first( 'id', 'first_name', diff --git a/backend/node/src/infrastructure/server/express/app.ts b/backend/node/src/infrastructure/server/express/app.ts index 9ce6a5b..0d26ab6 100644 --- a/backend/node/src/infrastructure/server/express/app.ts +++ b/backend/node/src/infrastructure/server/express/app.ts @@ -6,7 +6,11 @@ import { router } from './routes'; const app = express(); // JSON parser -app.use(json()); +app.use( + json({ + type: ['application/vnd.api+json', 'application/json'], + }) +); // Http logger app.use(httpLogger()); diff --git a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts index 3ce93fa..8390c33 100644 --- a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts +++ b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts @@ -1,6 +1,7 @@ import type { Request, Response } from 'express'; import { createReadStream } from 'fs'; import { extname } from 'path'; +import { DataDocument, Linker, Serializer } from 'ts-japi'; import { createUserController } from '../../../../adapters/controllers/create-user.controller'; import { findAllUsersController } from '../../../../adapters/controllers/find-all-users.controller'; import { findOneUserController } from '../../../../adapters/controllers/find-one-user.controller'; @@ -16,17 +17,33 @@ import type { UpdateUser, UserData, UserRequestParams, - UserResponse, } from '../../../../adapters/interfaces/user.interface'; +import { appConfig } from '../../../config/app'; import { userRepository } from '../../../repositories/user.repository'; import { fileService } from '../../../services/file.service'; +const userSerializer = new Serializer('users', { + version: '1.1', +}); +const userLinker = (type: 'multi' | 'single' = 'multi') => { + const UserLinker = new Linker<[UserData | UserData[]]>((users) => { + return type === 'multi' || Array.isArray(users) + ? `${appConfig.url}/users/` + : `${appConfig.url}/users/${users.id}`; + }); + + return UserLinker; +}; + export async function create( - req: Request, - res: Response> + req: Request>, + res: Response>> ) { let request: HttpRequestBody = { - body: req.body, + body: { + ...req.body.data.attributes, + id: req.body.data.id, + }, }; if (req.file) { @@ -45,83 +62,62 @@ export async function create( } const controller = createUserController(userRepository); - const { status, data } = await controller(request); - const { id, type, ...attributes } = data as UserResponse; - - res.status(status).json({ - jsonapi: { - version: '1.0', - }, - links: { - self: req.originalUrl, - }, - data: { - id, - type, - attributes, + const result = await userSerializer.serialize(data, { + linkers: { + resource: userLinker('single'), }, }); + + res.status(status).contentType('application/vnd.api+json').json(result); } export async function findAll( - req: Request, - res: Response> + _req: Request, + res: Response>> ) { const controller = findAllUsersController(userRepository, fileService); - const { status, data } = await controller({}); - res.status(status).json({ - jsonapi: { - version: '1.0', + const result = await userSerializer.serialize(data, { + linkers: { + resource: userLinker('multi'), }, - links: { - self: req.originalUrl, - }, - data: (data as UserResponse[]).map(({ id, type, ...obj }) => { - return { - id, - type, - attributes: obj, - }; - }), }); + + res.status(status).contentType('application/vnd.api+json').json(result); } export async function findOne( req: Request, - res: Response> + res: Response>> ) { const controller = findOneUserController(userRepository, fileService); - const { status, data } = await controller({ params: req.params }); - - const { id, type, ...attributes } = data as UserResponse; + const { status, data } = await controller({ + params: req.params, + }); - res.status(status).json({ - jsonapi: { - version: '1.0', - }, - links: { - self: req.originalUrl, - }, - data: { - id, - type, - attributes, + const result = await userSerializer.serialize(data, { + linkers: { + resource: userLinker('single'), }, }); + + res.status(status).contentType('application/vnd.api+json').json(result); } export async function update( - req: Request, - res: Response> + req: Request>, + res: Response>> ) { let request: HttpRequest = { params: req.params, - body: req.body, + body: { + ...req.body.data.attributes, + id: req.body.data.id, + }, }; if (req.file) { @@ -143,27 +139,21 @@ export async function update( const { status, data } = await controller(request); - const { id, type, ...attributes } = data as UserResponse; - - res.status(status).json({ - jsonapi: { - version: '1.0', - }, - links: { - self: req.originalUrl, - }, - data: { - id, - type, - attributes, + const result = await userSerializer.serialize(data, { + linkers: { + resource: userLinker('single'), }, }); + + res.status(status).contentType('application/vnd.api+json').json(result); } export async function remove(req: Request, res: Response) { const controller = removeUserController(userRepository, fileService); - const { status } = await controller({ params: req.params }); + const { status } = await controller({ + params: req.params, + }); - res.status(status).end(); + res.status(status).contentType('application/vnd.api+json').end(); } diff --git a/backend/node/src/infrastructure/server/express/middlewares/error.ts b/backend/node/src/infrastructure/server/express/middlewares/error.ts index df190d9..60c8ac9 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/error.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/error.ts @@ -1,6 +1,7 @@ import type { ErrorObject } from 'ajv'; import type { NextFunction, Request, Response } from 'express'; import { ValidationError } from 'express-json-validator-middleware'; +import { ReasonPhrases, StatusCodes } from 'http-status-codes'; import type { JsonApiError, JsonApiErrorObject, @@ -19,8 +20,8 @@ function createErrorArray(arr: ErrorObject[]): JsonApiErrorObject[] { .filter((item) => item.keyword !== 'if') .map((item) => { return { - status: 400, - title: 'Bad Request', + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, detail: item.message as string, }; }); @@ -33,7 +34,7 @@ export function errorHandler() { let response: JsonApiError = { jsonapi: { - version: '1.0', + version: '1.1', }, links: { self: req.path, diff --git a/backend/node/src/infrastructure/server/express/middlewares/logger.ts b/backend/node/src/infrastructure/server/express/middlewares/logger.ts index 784c06b..60c3600 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/logger.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/logger.ts @@ -4,7 +4,10 @@ import { log } from '../../../ports/logger'; export function httpLogger() { return (req: Request, res: Response, next: NextFunction) => { - log.http('HTTP log', { req, res }); + log.http('HTTP log', { + req, + res, + }); next(); }; @@ -17,7 +20,9 @@ export function errorLogger() { _res: Response, next: NextFunction ) => { - log.error('Error log', { err }); + log.error('Error log', { + err, + }); next(err); }; diff --git a/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts b/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts index 458f215..d2cf122 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts @@ -1,19 +1,34 @@ import type { NextFunction, Request, Response } from 'express'; import expressAsyncHandler from 'express-async-handler'; import { uniqueUserEmailController } from '../../../../adapters/controllers/unique-user-email.controller'; -import type { HttpRequestBody } from '../../../../adapters/interfaces/http.interface'; -import type { UpdateUserDto } from '../../../../core/interfaces/user.interface'; +import type { + HttpRequest, + JsonApiData, +} from '../../../../adapters/interfaces/http.interface'; +import type { + UpdateUserDto, + UserTable, +} from '../../../../core/interfaces/user.interface'; import { userRepository } from '../../../repositories/user.repository'; export function uniqueUserEmail() { return expressAsyncHandler( async ( - req: Request>, + req: Request< + Pick, + unknown, + JsonApiData> + >, _res: Response, next: NextFunction ) => { - const httpRequest: HttpRequestBody> = { - body: req.body, + const httpRequest: HttpRequest< + unknown, + Pick, + Pick + > = { + body: req.body.data.attributes, + params: req.params, }; const controller = uniqueUserEmailController(userRepository); diff --git a/backend/node/src/infrastructure/server/express/middlewares/validate-uuid.ts b/backend/node/src/infrastructure/server/express/middlewares/validate-uuid.ts index f893a38..aa3c3ec 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/validate-uuid.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/validate-uuid.ts @@ -10,7 +10,9 @@ export function validateUuid() { next: NextFunction ) => { const httpRequest: HttpRequestParams = { - params: { id: req.params.id }, + params: { + id: req.params.id, + }, }; validateUuidController(httpRequest); diff --git a/backend/node/src/infrastructure/server/express/package.json b/backend/node/src/infrastructure/server/express/package.json deleted file mode 100644 index 416c12c..0000000 --- a/backend/node/src/infrastructure/server/express/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "express-server", - "version": "0.0.1", - "private": true, - "dependencies": { - "ajv": "^8.11.0", - "ajv-errors": "3.0.0", - "ajv-formats": "2.1.1", - "cookie-parser": "1.4.6", - "express": "4.18.2", - "express-async-handler": "1.2.0", - "express-json-validator-middleware": "3.0.1", - "multer": "1.4.5-lts.1", - "swagger-ui-express": "5.0.0", - "yaml": "2.3.1" - }, - "devDependencies": { - "@types/cookie-parser": "1.4.3", - "@types/express": "4.17.17", - "@types/multer": "1.4.7", - "@types/swagger-ui-express": "4.1.3" - } -} diff --git a/backend/node/src/infrastructure/server/express/pnpm-lock.yaml b/backend/node/src/infrastructure/server/express/pnpm-lock.yaml deleted file mode 100644 index 1b5bb85..0000000 --- a/backend/node/src/infrastructure/server/express/pnpm-lock.yaml +++ /dev/null @@ -1,781 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - ajv: - specifier: ^8.11.0 - version: 8.12.0 - ajv-errors: - specifier: 3.0.0 - version: 3.0.0(ajv@8.12.0) - ajv-formats: - specifier: 2.1.1 - version: 2.1.1(ajv@8.12.0) - cookie-parser: - specifier: 1.4.6 - version: 1.4.6 - express: - specifier: 4.18.2 - version: 4.18.2 - express-async-handler: - specifier: 1.2.0 - version: 1.2.0 - express-json-validator-middleware: - specifier: 3.0.1 - version: 3.0.1 - multer: - specifier: 1.4.5-lts.1 - version: 1.4.5-lts.1 - swagger-ui-express: - specifier: 5.0.0 - version: 5.0.0(express@4.18.2) - yaml: - specifier: 2.3.1 - version: 2.3.1 - -devDependencies: - '@types/cookie-parser': - specifier: 1.4.3 - version: 1.4.3 - '@types/express': - specifier: 4.17.17 - version: 4.17.17 - '@types/multer': - specifier: 1.4.7 - version: 1.4.7 - '@types/swagger-ui-express': - specifier: 4.1.3 - version: 4.1.3 - -packages: - - /@types/body-parser@1.19.5: - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.9.1 - - /@types/connect@3.4.38: - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - dependencies: - '@types/node': 20.9.1 - - /@types/cookie-parser@1.4.3: - resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==} - dependencies: - '@types/express': 4.17.17 - dev: true - - /@types/express-serve-static-core@4.17.41: - resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} - dependencies: - '@types/node': 20.9.1 - '@types/qs': 6.9.10 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - - /@types/express@4.17.17: - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.17.41 - '@types/qs': 6.9.10 - '@types/serve-static': 1.15.5 - - /@types/http-errors@2.0.4: - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - - /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: false - - /@types/mime@1.3.5: - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - - /@types/mime@3.0.4: - resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} - - /@types/multer@1.4.7: - resolution: {integrity: sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==} - dependencies: - '@types/express': 4.17.17 - dev: true - - /@types/node@20.9.1: - resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} - dependencies: - undici-types: 5.26.5 - - /@types/qs@6.9.10: - resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} - - /@types/range-parser@1.2.7: - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - - /@types/send@0.17.4: - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.9.1 - - /@types/serve-static@1.15.5: - resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} - dependencies: - '@types/http-errors': 2.0.4 - '@types/mime': 3.0.4 - '@types/node': 20.9.1 - - /@types/swagger-ui-express@4.1.3: - resolution: {integrity: sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==} - dependencies: - '@types/express': 4.17.17 - '@types/serve-static': 1.15.5 - dev: true - - /accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - dev: false - - /ajv-errors@3.0.0(ajv@8.12.0): - resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} - peerDependencies: - ajv: ^8.0.1 - dependencies: - ajv: 8.12.0 - dev: false - - /ajv-formats@2.1.1(ajv@8.12.0): - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - dependencies: - ajv: 8.12.0 - dev: false - - /ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - dev: false - - /append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - dev: false - - /array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - dev: false - - /body-parser@1.20.1: - resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false - - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: false - - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - - /bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - dev: false - - /call-bind@1.0.5: - resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} - dependencies: - function-bind: 1.1.2 - get-intrinsic: 1.2.2 - set-function-length: 1.1.1 - dev: false - - /concat-stream@1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 2.3.8 - typedarray: 0.0.6 - dev: false - - /content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - dev: false - - /cookie-parser@1.4.6: - resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} - engines: {node: '>= 0.8.0'} - dependencies: - cookie: 0.4.1 - cookie-signature: 1.0.6 - dev: false - - /cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - dev: false - - /cookie@0.4.1: - resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} - engines: {node: '>= 0.6'} - dev: false - - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: false - - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: false - - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - dev: false - - /define-data-property@1.1.1: - resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.2 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - dev: false - - /depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dev: false - - /destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dev: false - - /ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: false - - /encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - dev: false - - /escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: false - - /etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - dev: false - - /express-async-handler@1.2.0: - resolution: {integrity: sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==} - dev: false - - /express-json-validator-middleware@3.0.1: - resolution: {integrity: sha512-DkqrIwS4O1eCqshuBNG76dBV+BuoXhgsiUjW9Rh3aBerlIY6fwIrNQ+UZLqh6CLbqcQRQTbqiNA5AlneqlUflA==} - engines: {node: '>=14'} - dependencies: - '@types/express': 4.17.17 - '@types/express-serve-static-core': 4.17.41 - '@types/json-schema': 7.0.15 - ajv: 8.12.0 - dev: false - - /express@4.18.2: - resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} - engines: {node: '>= 0.10.0'} - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.1 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.5.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.2.0 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.1 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: 2.0.7 - qs: 6.11.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: false - - /finalhandler@1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} - engines: {node: '>= 0.8'} - dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false - - /forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - dev: false - - /fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - dev: false - - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: false - - /get-intrinsic@1.2.2: - resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} - dependencies: - function-bind: 1.1.2 - has-proto: 1.0.1 - has-symbols: 1.0.3 - hasown: 2.0.0 - dev: false - - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - dependencies: - get-intrinsic: 1.2.2 - dev: false - - /has-property-descriptors@1.0.1: - resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} - dependencies: - get-intrinsic: 1.2.2 - dev: false - - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - dev: false - - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: false - - /hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 - dev: false - - /http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - dev: false - - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: false - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - dev: false - - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: false - - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: false - - /media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - dev: false - - /merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - dev: false - - /methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - dev: false - - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: false - - /mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - dev: false - - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: false - - /mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: false - - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: false - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: false - - /multer@1.4.5-lts.1: - resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} - engines: {node: '>= 6.0.0'} - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 1.6.2 - mkdirp: 0.5.6 - object-assign: 4.1.1 - type-is: 1.6.18 - xtend: 4.0.2 - dev: false - - /negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - dev: false - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false - - /object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} - dev: false - - /on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - dependencies: - ee-first: 1.1.1 - dev: false - - /parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - dev: false - - /path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - dev: false - - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: false - - /proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - dev: false - - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - dev: false - - /qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.4 - dev: false - - /range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - dev: false - - /raw-body@2.5.1: - resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} - engines: {node: '>= 0.8'} - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - dev: false - - /readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - dev: false - - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: false - - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: false - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: false - - /send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} - engines: {node: '>= 0.8.0'} - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} - engines: {node: '>= 0.8.0'} - dependencies: - encodeurl: 1.0.2 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.18.0 - transitivePeerDependencies: - - supports-color - dev: false - - /set-function-length@1.1.1: - resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.1 - get-intrinsic: 1.2.2 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - dev: false - - /setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: false - - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - object-inspect: 1.13.1 - dev: false - - /statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - dev: false - - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - dependencies: - safe-buffer: 5.1.2 - dev: false - - /swagger-ui-dist@5.10.0: - resolution: {integrity: sha512-PBTn5qDOQVtU29hrx74km86SnK3/mFtF3grI98y575y1aRpxiuStRTIvsfXFudPFkLofHU7H9a+fKrP+Oayc3g==} - dev: false - - /swagger-ui-express@5.0.0(express@4.18.2): - resolution: {integrity: sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==} - engines: {node: '>= v0.10.32'} - peerDependencies: - express: '>=4.0.0 || >=5.0.0-beta' - dependencies: - express: 4.18.2 - swagger-ui-dist: 5.10.0 - dev: false - - /toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - dev: false - - /type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - dev: false - - /typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - dev: false - - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - /unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - dev: false - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.1 - dev: false - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false - - /utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - dev: false - - /vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - dev: false - - /xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - dev: false - - /yaml@2.3.1: - resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} - engines: {node: '>= 14'} - dev: false diff --git a/backend/node/src/infrastructure/server/express/routes/index.ts b/backend/node/src/infrastructure/server/express/routes/index.ts index 66d0f74..edf5f91 100644 --- a/backend/node/src/infrastructure/server/express/routes/index.ts +++ b/backend/node/src/infrastructure/server/express/routes/index.ts @@ -6,6 +6,6 @@ const router = Router(); router.use('/', appRouter); -router.use('/user', userRouter); +router.use('/users', userRouter); export { router }; diff --git a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts index 68ff6c7..b13426a 100644 --- a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts +++ b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts @@ -3,32 +3,105 @@ import type { AllowedSchema } from 'express-json-validator-middleware'; export const createUserSchema: AllowedSchema = { type: 'object', properties: { - first_name: { - type: 'string', - minLength: 1, + jsonapi: { + type: 'object', + properties: { + version: { + type: 'string', + errorMessage: { + type: 'The JSON:API version must be a string.', + }, + }, + }, + required: ['version'], + errorMessage: { + required: { + version: 'The JSON:API version is required.', + }, + }, }, - last_name: { - type: 'string', - minLength: 1, + data: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + errorMessage: { + format: 'The data.id property must be a UUID (version 4).', + }, + }, + type: { + type: 'string', + pattern: 'users', + errorMessage: { + pattern: `The data.type property must be 'users'`, + }, + }, + attributes: { + type: 'object', + properties: { + first_name: { + type: 'string', + minLength: 1, + }, + last_name: { + type: 'string', + minLength: 1, + }, + email: { + type: 'string', + minLength: 1, + format: 'email', + errorMessage: { + minLength: 'The data.attributes.email property is required.', + format: + 'The data.attributes.email property must be a valid email address.', + }, + }, + }, + required: ['first_name', 'email'], + errorMessage: { + required: { + first_name: + 'The data.attributes.first_name property is required.', + email: 'The data.attributes.email property is required.', + }, + properties: { + first_name: + 'The data.attributes.first_name property is required.', + }, + }, + }, + }, + required: ['type'], + errorMessage: { + required: { + type: 'The data.type property is required.', + }, + }, }, - email: { - type: 'string', - minLength: 1, - format: 'email', + links: { + type: 'object', + properties: { + self: { + type: 'string', + errorMessage: { + type: 'The links.self property must be a string.', + }, + }, + }, + required: ['self'], errorMessage: { - minLength: 'The email is required.', - format: 'The email must be a valid email address.', + required: { + self: 'The links.self property is required.', + }, }, }, }, - required: ['first_name', 'email'], + required: ['data'], errorMessage: { required: { - first_name: 'The first name is required.', - email: 'The email is required.', - }, - properties: { - first_name: 'The first name is required.', + data: 'The data property is required.', }, }, }; @@ -36,33 +109,103 @@ export const createUserSchema: AllowedSchema = { export const updateUserSchema: AllowedSchema = { type: 'object', properties: { - first_name: { - type: 'string', - minLength: 1, + jsonapi: { + type: 'object', + properties: { + version: { + type: 'string', + errorMessage: { + type: 'The JSON:API version must be a string.', + }, + }, + }, + required: ['version'], errorMessage: { - type: 'The first name must be a string.', - minLength: 'The first name field must have a value.', + required: { + version: 'The JSON:API version is required.', + }, }, }, - last_name: { - type: 'string', - nullable: true, - }, - nickname: { - type: 'string', - nullable: true, + data: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + errorMessage: { + format: 'The data.id property must be a UUID (version 4).', + }, + }, + type: { + type: 'string', + pattern: 'users', + errorMessage: { + pattern: `The data.type property must be 'users'`, + }, + }, + attributes: { + type: 'object', + properties: { + first_name: { + type: 'string', + minLength: 1, + errorMessage: { + type: 'The data.attributes.first_name must be a string.', + minLength: + 'The data.attributes.first_name field must have a value.', + }, + }, + last_name: { + type: 'string', + nullable: true, + }, + nickname: { + type: 'string', + nullable: true, + }, + email: { + type: 'string', + format: 'email', + errorMessage: { + type: 'The data.attributes.email must be a string.', + format: + 'The data.attributes.email must be a valid email address.', + }, + }, + }, + }, + }, + required: ['type'], + errorMessage: { + required: { + type: 'The data.type property is required.', + }, + }, }, - email: { - type: 'string', - format: 'email', + links: { + type: 'object', + properties: { + self: { + type: 'string', + errorMessage: { + type: 'The links.self property must be a string.', + }, + }, + }, + required: ['self'], errorMessage: { - type: 'The email must be a string.', - format: 'The email must be a valid email address.', + required: { + self: 'The links.self property is required.', + }, }, }, }, + required: ['data'], dependencies: {}, errorMessage: { + required: { + data: 'The data property is required.', + }, dependencies: {}, }, }; diff --git a/backend/node/src/infrastructure/services/file.service.ts b/backend/node/src/infrastructure/services/file.service.ts index c16a0b4..cf23c84 100644 --- a/backend/node/src/infrastructure/services/file.service.ts +++ b/backend/node/src/infrastructure/services/file.service.ts @@ -4,8 +4,8 @@ import { PutObjectCommand, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import type { FileService } from '../../core/interfaces/file.interface'; import type { File } from '../../core/entities/common.entity'; +import type { FileService } from '../../core/interfaces/file.interface'; import { s3 } from '../ports/s3'; export const fileService: FileService = { @@ -36,6 +36,10 @@ export const fileService: FileService = { new GetObjectCommand({ Bucket: 'local', Key: path }) ); + if (!url) { + return null; + } + return url; }, diff --git a/backend/node/tsconfig.base.json b/backend/node/tsconfig.base.json index e813137..16f3e57 100644 --- a/backend/node/tsconfig.base.json +++ b/backend/node/tsconfig.base.json @@ -1,5 +1,5 @@ { - "extends": ["@tsconfig/strictest/tsconfig", "@tsconfig/node18/tsconfig"], + "extends": ["@tsconfig/strictest/tsconfig", "@tsconfig/node20/tsconfig"], "include": ["./src/**/*.ts"], "compilerOptions": { "baseUrl": "./", @@ -9,13 +9,6 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "noPropertyAccessFromIndexSignature": false, - "removeComments": true, - "typeRoots": [ - "./src/infrastructure/server/express/types", - "./src/infrastructure/server/express/node_modules/@types", - "./src/infrastructure/server/fastify/types", - "./src/infrastructure/server/fastify/node_modules/@types", - "./node_modules/@types" - ] + "removeComments": true } } diff --git a/backend/node/tsconfig.build.json b/backend/node/tsconfig.build.json new file mode 100644 index 0000000..37bf7a6 --- /dev/null +++ b/backend/node/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.base.json", + "exclude": ["./src/**/*.spec.ts"] +} diff --git a/backend/node/tsconfig.eslint.json b/backend/node/tsconfig.eslint.json index 82dc71e..926c97f 100644 --- a/backend/node/tsconfig.eslint.json +++ b/backend/node/tsconfig.eslint.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.base.json", "include": [ "./src/**/*.ts", - "./__tests__/**/*", + "./__tests__/**/*.ts", "./*.ts", "./*.js", "./.*.js", diff --git a/backend/node/tsconfig.json b/backend/node/tsconfig.json index 37bf7a6..99bf39f 100644 --- a/backend/node/tsconfig.json +++ b/backend/node/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.base.json", - "exclude": ["./src/**/*.spec.ts"] + "include": ["./src/**/*.ts", "./__tests__/**/*.ts"] } diff --git a/backend/node/tsconfig.tsnode.json b/backend/node/tsconfig.tsnode.json deleted file mode 100644 index edaff4e..0000000 --- a/backend/node/tsconfig.tsnode.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "allowUnusedLabels": false, - "allowUnreachableCode": false, - "exactOptionalPropertyTypes": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "isolatedModules": true, - "checkJs": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "lib": ["es2023"], - "module": "node16", - "target": "es2022", - "moduleResolution": "node16", - "baseUrl": "./", - "outDir": "./dist", - "incremental": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "removeComments": true, - "typeRoots": [ - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/types", - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/express/node_modules/@types", - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/types", - "/home/husen/Projects/playground/backend/node/src/infrastructure/server/fastify/node_modules/@types", - "/home/husen/Projects/playground/backend/node/node_modules/@types" - ] - }, - "files": [ - "./src/adapters/controllers/create-user.controller.ts", - "./src/adapters/controllers/find-all-users.controller.ts", - "./src/adapters/controllers/find-one-user.controller.ts", - "./src/adapters/controllers/home.controller.ts", - "./src/adapters/controllers/remove-user.controller.ts", - "./src/adapters/controllers/unique-user-email.controller.ts", - "./src/adapters/controllers/update-user.controller.ts", - "./src/adapters/controllers/validate-uuid.controller.ts", - "./src/adapters/interfaces/common.interface.ts", - "./src/adapters/interfaces/http.interface.ts", - "./src/adapters/interfaces/user.interface.ts", - "./src/core/entities/common.entity.ts", - "./src/core/entities/user.entity.ts", - "./src/core/exceptions/bad-request.exception.ts", - "./src/core/exceptions/http.exception.ts", - "./src/core/exceptions/not-found.exception.ts", - "./src/core/interfaces/common.interface.ts", - "./src/core/interfaces/file.interface.ts", - "./src/core/interfaces/http.interface.ts", - "./src/core/interfaces/redis.interface.ts", - "./src/core/interfaces/user.interface.ts", - "./src/core/use-cases/create-user.use-case.ts", - "./src/core/use-cases/find-all-users.use-case.ts", - "./src/core/use-cases/find-one-user.use-case.ts", - "./src/core/use-cases/remove-user.use-case.ts", - "./src/core/use-cases/unique-user-email.use-case.ts", - "./src/core/use-cases/update-user.use-case.ts", - "./src/core/use-cases/validate-uuid.use-case.ts", - "./src/infrastructure/config/app.ts", - "./src/infrastructure/config/auth.ts", - "./src/infrastructure/config/database.ts", - "./src/infrastructure/config/redis.ts", - "./src/infrastructure/config/s3.ts", - "./src/infrastructure/ports/database.ts", - "./src/infrastructure/ports/logger.ts", - "./src/infrastructure/ports/redis.ts", - "./src/infrastructure/ports/s3.ts", - "./src/infrastructure/repositories/user.repository.ts", - "./src/infrastructure/server/express/app.ts", - "./src/infrastructure/server/express/index.ts", - "./src/infrastructure/server/express/handlers/app.handler.ts", - "./src/infrastructure/server/express/handlers/user.handler.ts", - "./src/infrastructure/server/express/middlewares/error.ts", - "./src/infrastructure/server/express/middlewares/logger.ts", - "./src/infrastructure/server/express/middlewares/unique-user-email.ts", - "./src/infrastructure/server/express/middlewares/validate-uuid.ts", - "./src/infrastructure/server/express/middlewares/validator.ts", - "./src/infrastructure/server/express/routes/app.route.ts", - "./src/infrastructure/server/express/routes/index.ts", - "./src/infrastructure/server/express/routes/user.route.ts", - "./src/infrastructure/server/express/schemas/auth.schema.ts", - "./src/infrastructure/server/express/schemas/common.schema.ts", - "./src/infrastructure/server/express/schemas/user.schema.ts", - "./src/infrastructure/server/express/types/express/index.d.ts", - "./src/infrastructure/server/express/types/express-json-validator-middleware/index.d.ts", - "./src/infrastructure/services/file.service.ts", - "./src/infrastructure/services/redis.service.ts" - ], - "include": ["./src/**/*.ts"], - "exclude": ["./src/**/*.spec.ts"] -} diff --git a/backend/node/vitest.config.ts b/backend/node/vitest.config.ts deleted file mode 100644 index 30b3392..0000000 --- a/backend/node/vitest.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { defineConfig } from 'vitest/config'; - -const config = defineConfig({ - test: { - clearMocks: true, - include: ['**/src/**/*.spec.ts', '**/__tests__/**/*.test.ts'], - }, -}); - -// eslint-disable-next-line import/no-default-export -export default config; diff --git a/config/kratos/email-password/hooks/after-registration.jsonnet b/config/kratos/email-password/hooks/after-registration.jsonnet new file mode 100644 index 0000000..6b11409 --- /dev/null +++ b/config/kratos/email-password/hooks/after-registration.jsonnet @@ -0,0 +1,5 @@ +function(ctx) { + first_name: ctx.identity.traits.name.first, + last_name: ctx.identity.traits.name.last, + email: ctx.identity.traits.email, +} diff --git a/config/kratos/email-password/kratos.yml b/config/kratos/email-password/kratos.yml index 4ca37f9..4931258 100644 --- a/config/kratos/email-password/kratos.yml +++ b/config/kratos/email-password/kratos.yml @@ -68,6 +68,14 @@ selfservice: hooks: - hook: session - hook: show_verification_ui + - hook: web_hook + config: + url: http://backend:3000/user + method: POST + body: file:///etc/config/kratos/hooks/after-registration.jsonnet + response: + ignore: true + parse: false log: level: debug diff --git a/config/oathkeeper/access-rules.yml b/config/oathkeeper/access-rules.yml index fb6e613..1bf9751 100644 --- a/config/oathkeeper/access-rules.yml +++ b/config/oathkeeper/access-rules.yml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=https://github.com/imrushi/oathkeeper/raw/feat-rules-schema/.schema/rules.schema.json + - id: "ory:kratos:public" upstream: @@ -20,6 +22,27 @@ mutators: - handler: noop +- + id: "backend:anonymous" + upstream: + preserve_host: true + url: "http://backend:3000" + match: + url: "http://127.0.0.1:4455/<{user,user/*}>" + methods: + - GET + - POST + - DELETE + - PATCH + authenticators: + - + handler: anonymous + authorizer: + handler: allow + mutators: + - + handler: noop + - id: "ory:kratos-selfservice-ui-node:anonymous" upstream: diff --git a/docker-compose-express.yml b/docker-compose-express.yml index e69de29..db3b13a 100644 --- a/docker-compose-express.yml +++ b/docker-compose-express.yml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + backend-migrate: + image: playground-backend-express + depends_on: + - postgres + build: + context: ./ + dockerfile: ./express.Dockerfile + volumes: + - ./backend/node:/app/node + command: pnpm migrate:up + restart: on-failure + networks: + - playground + + backend-seeder: + image: playground-backend-express + depends_on: + - backend-migrate + build: + context: ./ + dockerfile: ./express.Dockerfile + volumes: + - ./backend/node:/app/node + command: pnpm seed:run + restart: on-failure + networks: + - playground + + backend: + image: playground-backend-express + depends_on: + - backend-migrate + - backend-seeder + build: + context: ./ + dockerfile: ./express.Dockerfile + ports: + - 3000:3000 + volumes: + - ./backend/node:/app/node + restart: on-failure + networks: + - playground diff --git a/docker-compose-ory.yml b/docker-compose-ory.yml index e76f915..82ef18c 100644 --- a/docker-compose-ory.yml +++ b/docker-compose-ory.yml @@ -1,12 +1,38 @@ version: '3.8' services: + postgres-kratos: + image: docker.io/library/postgres:16-alpine + ports: + - '${DB_PORT:-5431}:5432' + volumes: + - 'kratos-postgres:/var/lib/postgresql/data' + environment: + POSTGRES_USER: ${DB_USERNAME:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_DATABASE:-postgres} + healthcheck: + test: + [ + 'CMD', + 'pg_isready', + '-q', + '-d', + '${DB_USERNAME:-postgres}', + '-U', + '${DB_PASSWORD:-postgres}', + ] + retries: 3 + timeout: 5s + networks: + - playground + kratos-migrate: image: docker.io/oryd/kratos:v1.0.0 depends_on: - - postgres + - postgres-kratos environment: - - DSN=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://postgres:postgres@postgres-kratos:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 volumes: - type: volume source: kratos-sqlite @@ -29,7 +55,7 @@ services: - '4434:4434' # admin restart: unless-stopped environment: - - DSN=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://postgres:postgres@postgres-kratos:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 - LOG_LEVEL=trace - SERVE_PUBLIC_BASE_URL=http://127.0.0.1:4455/.ory/kratos/public/ command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier @@ -81,4 +107,5 @@ services: - ./config/oathkeeper:/etc/config/oathkeeper volumes: + kratos-postgres: kratos-sqlite: diff --git a/docker-compose-sveltekit.yml b/docker-compose-sveltekit.yml deleted file mode 100644 index e69de29..0000000 diff --git a/express.Dockerfile b/express.Dockerfile new file mode 100644 index 0000000..60b8f3f --- /dev/null +++ b/express.Dockerfile @@ -0,0 +1,10 @@ +FROM docker.io/library/node:20-bookworm +ADD ./openapi.yaml ./openapi.yaml +ADD ./backend/node /app/node +WORKDIR /app/node +RUN rm -rf node_modules +RUN rm -rf dist +RUN npm install -g pnpm + +EXPOSE 3000 +CMD ["pnpm", "dev"] diff --git a/openapi.yaml b/openapi.yaml index c54032a..1c40a57 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -16,7 +16,7 @@ info: url: https://www.apache.org/licenses/LICENSE-2.0.html version: 0.2.0 paths: - /user: + /users: post: tags: - "user" @@ -88,7 +88,7 @@ paths: - example: links: self: "/user" - /user/{id}: + /users/{id}: get: tags: - "user" diff --git a/shell.nix b/shell.nix index 28d9959..cedfe62 100644 --- a/shell.nix +++ b/shell.nix @@ -1,12 +1,20 @@ { pkgs ? import {} }: pkgs.mkShell { buildInputs = [ + pkgs.jsonnet-language-server + pkgs.yaml-language-server + pkgs.nil + pkgs.nixpkgs-fmt + pkgs.vscode-langservers-extracted + pkgs.dockerfile-language-server-nodejs + pkgs.marksman pkgs.go pkgs.gopls pkgs.gotools pkgs.delve - pkgs.nodejs + pkgs.nodejs_20 pkgs.nodePackages.pnpm pkgs.nodePackages.typescript-language-server + pkgs.nodePackages.bash-language-server ]; } diff --git a/templates.sublime-project b/templates.sublime-project deleted file mode 100644 index 4d625f0..0000000 --- a/templates.sublime-project +++ /dev/null @@ -1,18 +0,0 @@ -{ - "folders": [ - { - "path": ".", - } - ], - "settings": { - "LSP": { - "LSP-eslint": { - "settings": { - "workingDirectories": [ - "./node" - ], - }, - }, - }, - }, -} From b9a9cdcc1a774ff2630be294b49066525e6ca027 Mon Sep 17 00:00:00 2001 From: Husen Date: Sun, 4 Feb 2024 16:07:31 +0700 Subject: [PATCH 4/8] chore: implement openapi's pagination Signed-off-by: Husen --- backend/node/__tests__/app.test.ts | 21 +- backend/node/__tests__/setup.ts | 6 +- backend/node/__tests__/user.test.ts | 643 +++++++++++++----- backend/node/database/seeds/users.js | 14 +- backend/node/package.json | 2 + backend/node/pnpm-lock.yaml | 19 + .../create-user.controller.spec.ts | 3 +- .../find-all-users.controller.spec.ts | 73 +- .../controllers/find-all-users.controller.ts | 19 +- .../find-one-user.controller.spec.ts | 8 +- .../controllers/home.controller.spec.ts | 2 +- .../remove-user.controller.spec.ts | 8 +- .../unique-user-email.controller.spec.ts | 6 +- .../update-user.controller.spec.ts | 29 +- .../validate-uuid.controller.spec.ts | 2 +- .../adapters/interfaces/common.interface.ts | 2 + .../src/adapters/interfaces/http.interface.ts | 7 + .../src/core/entities/validation.entity.ts | 65 ++ .../src/core/interfaces/common.interface.ts | 4 +- .../src/core/interfaces/http.interface.ts | 12 + .../src/core/interfaces/user.interface.ts | 2 +- .../use-cases/create-user.use-case.spec.ts | 11 +- .../core/use-cases/create-user.use-case.ts | 4 +- .../use-cases/find-all-users.use-case.spec.ts | 111 ++- .../core/use-cases/find-all-users.use-case.ts | 23 +- .../use-cases/find-one-user.use-case.spec.ts | 23 +- .../core/use-cases/find-one-user.use-case.ts | 3 +- .../use-cases/remove-user.use-case.spec.ts | 17 +- .../core/use-cases/remove-user.use-case.ts | 3 +- .../unique-user-email.use-case.spec.ts | 6 +- .../use-cases/unique-user-email.use-case.ts | 3 +- .../use-cases/update-user.use-case.spec.ts | 17 +- .../core/use-cases/update-user.use-case.ts | 3 +- .../use-cases/validate-uuid.use-case.spec.ts | 4 +- .../repositories/user.repository.ts | 44 +- .../src/infrastructure/server/express/app.ts | 11 + .../server/express/handlers/user.handler.ts | 54 +- .../server/express/middlewares/validator.ts | 16 +- .../server/express/routes/user.route.ts | 12 +- .../server/express/schemas/auth.schema.ts | 26 - .../server/express/schemas/common.schema.ts | 8 +- .../server/express/schemas/user.schema.ts | 98 ++- .../hooks/after-registration.jsonnet | 12 +- .../email-password/identity.schema.json | 3 +- config/kratos/email-password/kratos.yml | 6 +- config/oathkeeper/access-rules.yml | 2 +- docker-compose-express.yml | 6 + docker-compose-ory.yml | 8 - openapi.yaml | 18 + 49 files changed, 1114 insertions(+), 385 deletions(-) create mode 100644 backend/node/src/core/entities/validation.entity.ts delete mode 100644 backend/node/src/infrastructure/server/express/schemas/auth.schema.ts diff --git a/backend/node/__tests__/app.test.ts b/backend/node/__tests__/app.test.ts index a194ba8..2e66fe0 100644 --- a/backend/node/__tests__/app.test.ts +++ b/backend/node/__tests__/app.test.ts @@ -1,5 +1,6 @@ -import { StatusCodes } from 'http-status-codes'; -import { request } from './setup'; +import { ReasonPhrases, StatusCodes } from 'http-status-codes'; +import { JsonApiError } from '../src/adapters/interfaces/http.interface'; +import { SupertestResponse, request } from './setup'; import { close } from './teardown'; afterAll(async () => { @@ -8,23 +9,25 @@ afterAll(async () => { describe('GET /', () => { test('should return hello world', async () => { - const response = await request + const response: SupertestResponse = await request .get('/') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.OK); - expect(response.body).toEqual('Hello world!'); + expect(response.status).toStrictEqual(StatusCodes.OK); + expect(response.body).toStrictEqual('Hello world!'); }); }); describe('GET /404', () => { test('should return not found error', async () => { - const response = await request + const response: SupertestResponse = await request .get('/404') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.NOT_FOUND); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.NOT_FOUND + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -34,7 +37,7 @@ describe('GET /404', () => { errors: [ { status: StatusCodes.NOT_FOUND, - title: 'Not Found', + title: ReasonPhrases.NOT_FOUND, detail: 'Resource not found.', }, ], diff --git a/backend/node/__tests__/setup.ts b/backend/node/__tests__/setup.ts index d8d1fa0..a7ec774 100644 --- a/backend/node/__tests__/setup.ts +++ b/backend/node/__tests__/setup.ts @@ -1,4 +1,4 @@ -import supertest from 'supertest'; +import supertest, { Response } from 'supertest'; import { app } from '../src/infrastructure/server/express/app'; import { setup as userSetup } from './fixtures/user.fixture'; @@ -7,3 +7,7 @@ export async function setup() { } export const request = supertest(app); + +export interface SupertestResponse extends Omit { + body: Body; +} diff --git a/backend/node/__tests__/user.test.ts b/backend/node/__tests__/user.test.ts index b009024..b29b1e2 100644 --- a/backend/node/__tests__/user.test.ts +++ b/backend/node/__tests__/user.test.ts @@ -1,6 +1,13 @@ import { ReasonPhrases, StatusCodes } from 'http-status-codes'; +import { DataDocument } from 'ts-japi'; +import { + JsonApiError, + JsonApiPagination, +} from '../src/adapters/interfaces/http.interface'; +import { UserData } from '../src/adapters/interfaces/user.interface'; +import { getErrorMessage } from '../src/core/entities/validation.entity'; import { setup, testUser, user } from './fixtures/user.fixture'; -import { request } from './setup'; +import { SupertestResponse, request } from './setup'; import { close } from './teardown'; beforeEach(async () => { @@ -13,12 +20,14 @@ afterAll(async () => { describe('POST /users', () => { test('should return error when request body empty', async () => { - const response = await request + const response: SupertestResponse = await request .post('/users') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -29,14 +38,14 @@ describe('POST /users', () => { { status: StatusCodes.BAD_REQUEST, title: ReasonPhrases.BAD_REQUEST, - detail: 'The data property is required.', + detail: getErrorMessage('data.required'), }, ], }); }); test('should return error when email is invalid', async () => { - const response = await request + const response: SupertestResponse = await request .post('/users') .set('Accept', 'application/vnd.api+json') .send({ @@ -50,8 +59,10 @@ describe('POST /users', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -61,16 +72,15 @@ describe('POST /users', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: - 'The data.attributes.email property must be a valid email address.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.format'), }, ], }); }); test('should return error when email is already taken', async () => { - const response = await request + const response: SupertestResponse = await request .post('/users') .set('Accept', 'application/vnd.api+json') .send({ @@ -84,8 +94,10 @@ describe('POST /users', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -95,15 +107,15 @@ describe('POST /users', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The email has already been taken.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('email.unique'), }, ], }); }); test('should create an user', async () => { - const response = await request + const response: SupertestResponse> = await request .post('/users') .set('Accept', 'application/vnd.api+json') .send({ @@ -117,67 +129,266 @@ describe('POST /users', () => { }, }); - expect(response.status).toEqual(StatusCodes.CREATED); - expect(response.body).toHaveProperty('jsonapi.version', '1.1'); - expect(response.body).toHaveProperty('data.id'); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toStrictEqual( + StatusCodes.CREATED + ); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id' + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( 'data.attributes.first_name', testUser.first_name ); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data.attributes.last_name', - testUser.last_name + String(testUser.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname' ); - expect(response.body).toHaveProperty('data.attributes.nickname'); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data.attributes.email', testUser.email ); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); }); }); describe('GET /users', () => { + test('should return error when query parameter is empty', async () => { + const response: SupertestResponse = await request + .get('/users') + .set('Accept', 'application/vnd.api+json'); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + links: { + self: '/users', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('page.required'), + }, + ], + }); + }); + + test('should return error when page query parameter is empty', async () => { + const response: SupertestResponse = await request + .get('/users') + .query({ + page: null, + }) + .set('Accept', 'application/vnd.api+json'); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + links: { + self: '/users', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('page.type'), + }, + ], + }); + }); + + test('should return error when page number query parameter is mising', async () => { + const response: SupertestResponse = await request + .get('/users') + .query({ + page: { + size: 10, + }, + }) + .set('Accept', 'application/vnd.api+json'); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + links: { + self: '/users', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('page.number.required'), + }, + ], + }); + }); + + test('should return error when page size query parameter is mising', async () => { + const response: SupertestResponse = await request + .get('/users') + .query({ + page: { + number: 10, + }, + }) + .set('Accept', 'application/vnd.api+json'); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + links: { + self: '/users', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('page.size.required'), + }, + ], + }); + }); + + test('should return error when page size & number query parameter is wrong type', async () => { + const response: SupertestResponse = await request + .get('/users') + .query({ + page: { + number: null, + size: null, + }, + }) + .set('Accept', 'application/vnd.api+json'); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + links: { + self: '/users', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('page.number.type'), + }, + { + status: StatusCodes.BAD_REQUEST, + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('page.size.type'), + }, + ], + }); + }); + test('should return users', async () => { - const response = await request + const params: JsonApiPagination = { + page: { + number: 1, + size: 10, + }, + }; + + const response: SupertestResponse> = await request .get('/users') + .query(params) .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.OK); - expect(response.body).toHaveProperty('jsonapi.version', '1.1'); - expect(response.body).toHaveProperty('data[0].type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data[0].type', + 'users' + ); + expect>(response.body).toHaveProperty( 'data[0].attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data[0].attributes.last_name', - user.last_name + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data[0].attributes.nickname' ); - expect(response.body).toHaveProperty('data[0].attributes.nickname'); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data[0].attributes.email', user.email ); - expect(response.body).toHaveProperty('data[0].attributes.photo', null); - expect(response.body).toHaveProperty('data[0].attributes.avatar', null); - expect(response.body).toHaveProperty('data[0].attributes.created_at'); - expect(response.body).toHaveProperty('data[0].attributes.updated_at'); + expect>(response.body).toHaveProperty( + 'data[0].attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data[0].attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data[0].attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data[0].attributes.updated_at' + ); }); }); describe('GET /users/{id}', () => { test('should return error when parameter is invalid', async () => { - const response = await request + const response: SupertestResponse = await request .get('/users/invalid-id') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -187,20 +398,22 @@ describe('GET /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'Validation failed (uuid v4 is expected).', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('id.format'), }, ], }); }); test('should return error when user is not found', async () => { - const response = await request + const response: SupertestResponse = await request .get('/users/60677a98-a65e-4abc-831c-45dd76e8f990') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.NOT_FOUND); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.NOT_FOUND + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -210,47 +423,73 @@ describe('GET /users/{id}', () => { errors: [ { status: StatusCodes.NOT_FOUND, - title: 'Not Found', - detail: 'The user is not found.', + title: ReasonPhrases.NOT_FOUND, + detail: getErrorMessage('user.exist'), }, ], }); }); test('should return an user', async () => { - const response = await request + const response: SupertestResponse> = await request .get(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.OK); - expect(response.body).toHaveProperty('jsonapi.version', '1.1'); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id', + user.id + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data.attributes.last_name', - user.last_name - ); - expect(response.body).toHaveProperty('data.attributes.nickname'); - expect(response.body).toHaveProperty('data.attributes.email', user.email); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.email', + user.email + ); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); }); }); describe('PATCH /users/{id}', () => { test('should return error when parameter is invalid', async () => { - const response = await request + const response: SupertestResponse = await request .patch('/users/invalid-id') .set('Accept', 'application/vnd.api+json'); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -260,15 +499,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'Validation failed (uuid v4 is expected).', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('id.format'), }, ], }); }); test('should return error when user is not found', async () => { - const response = await request + const response: SupertestResponse = await request .patch('/users/60677a98-a65e-4abc-831c-45dd76e8f990') .set('Accept', 'application/vnd.api+json') .send({ @@ -277,8 +516,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.NOT_FOUND); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.NOT_FOUND + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -289,14 +530,14 @@ describe('PATCH /users/{id}', () => { { status: StatusCodes.NOT_FOUND, title: ReasonPhrases.NOT_FOUND, - detail: 'The user is not found.', + detail: getErrorMessage('user.exist'), }, ], }); }); test('should return error when name is null', async () => { - const response = await request + const response: SupertestResponse = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -308,8 +549,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -319,15 +562,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The data.attributes.first_name must be a string.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.first_name.type'), }, ], }); }); test('should return error when name is empty', async () => { - const response = await request + const response: SupertestResponse = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -339,8 +582,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -350,15 +595,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The data.attributes.first_name field must have a value.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.first_name.minLength'), }, ], }); }); test('should return error when name is invalid', async () => { - const response = await request + const response: SupertestResponse = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -370,8 +615,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -381,15 +628,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The data.attributes.first_name must be a string.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.first_name.type'), }, ], }); }); test('should ok when username is null', async () => { - const response = await request + const response: SupertestResponse> = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -401,28 +648,53 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.OK); - expect(response.body).toHaveProperty('jsonapi.version', '1.1'); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id', + user.id + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data.attributes.last_name', - user.last_name - ); - expect(response.body).toHaveProperty('data.attributes.nickname', null); - expect(response.body).toHaveProperty('data.attributes.email', user.email); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.email', + user.email + ); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); }); test('should ok when username is empty', async () => { - const response = await request + const response: SupertestResponse> = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -434,28 +706,53 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.OK); - expect(response.body).toHaveProperty('jsonapi.version', '1.1'); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id', + user.id + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data.attributes.last_name', - user.last_name - ); - expect(response.body).toHaveProperty('data.attributes.nickname', null); - expect(response.body).toHaveProperty('data.attributes.email', user.email); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.email', + user.email + ); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); }); test('should return error when email is null', async () => { - const response = await request + const response: SupertestResponse = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -467,8 +764,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -478,15 +777,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The data.attributes.email must be a string.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.type'), }, ], }); }); test('should return error when email is empty', async () => { - const response = await request + const response: SupertestResponse = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -498,8 +797,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -509,15 +810,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The data.attributes.email must be a valid email address.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.format'), }, ], }); }); test('should return error when email is invalid', async () => { - const response = await request + const response: SupertestResponse = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -529,8 +830,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -540,15 +843,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The data.attributes.email must be a string.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.type'), }, ], }); }); test('should return error when email is invalid', async () => { - const response = await request + const response: SupertestResponse = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -560,8 +863,10 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ jsonapi: { version: '1.1', }, @@ -571,15 +876,15 @@ describe('PATCH /users/{id}', () => { errors: [ { status: StatusCodes.BAD_REQUEST, - title: 'Bad Request', - detail: 'The data.attributes.email must be a valid email address.', + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.format'), }, ], }); }); test('should not update an user', async () => { - const response = await request + const response: SupertestResponse> = await request .patch(`/users/${user.id}`) .set('Accept', 'application/vnd.api+json') .send({ @@ -588,23 +893,47 @@ describe('PATCH /users/{id}', () => { }, }); - expect(response.status).toEqual(StatusCodes.OK); - expect(response.body).toHaveProperty('jsonapi.version', '1.1'); - expect(response.body).toHaveProperty('data.id', user.id); - expect(response.body).toHaveProperty('data.type', 'users'); - expect(response.body).toHaveProperty( + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id', + user.id + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( 'data.attributes.first_name', user.first_name ); - expect(response.body).toHaveProperty( + expect>(response.body).toHaveProperty( 'data.attributes.last_name', - user.last_name - ); - expect(response.body).toHaveProperty('data.attributes.nickname'); - expect(response.body).toHaveProperty('data.attributes.email', user.email); - expect(response.body).toHaveProperty('data.attributes.photo', null); - expect(response.body).toHaveProperty('data.attributes.avatar', null); - expect(response.body).toHaveProperty('data.attributes.created_at'); - expect(response.body).toHaveProperty('data.attributes.updated_at'); + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.email', + user.email + ); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); }); }); diff --git a/backend/node/database/seeds/users.js b/backend/node/database/seeds/users.js index 5389532..5ddbf12 100644 --- a/backend/node/database/seeds/users.js +++ b/backend/node/database/seeds/users.js @@ -1,4 +1,5 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires +// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires +const { faker } = require('@faker-js/faker'); /** * @param { import("knex").Knex } knex @@ -13,4 +14,15 @@ exports.seed = async function seed(knex) { nickname: 'John', email: 'johndoe@example.com', }); + + // eslint-disable-next-line no-plusplus + for (let index = 0; index < 50; index++) { + // eslint-disable-next-line no-await-in-loop + await knex('users').insert({ + first_name: faker.person.firstName(), + last_name: faker.person.lastName(), + nickname: faker.person.firstName(), + email: faker.internet.email().toLowerCase(), + }); + } }; diff --git a/backend/node/package.json b/backend/node/package.json index ba69338..c13b599 100644 --- a/backend/node/package.json +++ b/backend/node/package.json @@ -45,6 +45,7 @@ "express": "^4.18.2", "express-async-handler": "^1.2.0", "express-json-validator-middleware": "^3.0.1", + "express-query-parser": "^1.3.3", "fast-jwt": "1.7.2", "http-status-codes": "^2.3.0", "ioredis": "5.3.2", @@ -57,6 +58,7 @@ "yaml": "^2.3.4" }, "devDependencies": { + "@faker-js/faker": "^8.4.0", "@swc-node/register": "^1.6.8", "@swc/core": "^1.3.77", "@swc/helpers": "^0.5.1", diff --git a/backend/node/pnpm-lock.yaml b/backend/node/pnpm-lock.yaml index ce835f8..8184e7c 100644 --- a/backend/node/pnpm-lock.yaml +++ b/backend/node/pnpm-lock.yaml @@ -44,6 +44,9 @@ dependencies: express-json-validator-middleware: specifier: ^3.0.1 version: 3.0.1 + express-query-parser: + specifier: ^1.3.3 + version: 1.3.3 fast-jwt: specifier: 1.7.2 version: 1.7.2 @@ -76,6 +79,9 @@ dependencies: version: 2.3.4 devDependencies: + '@faker-js/faker': + specifier: ^8.4.0 + version: 8.4.0 '@swc-node/register': specifier: ^1.6.8 version: 1.6.8(@swc/core@1.3.96)(typescript@5.1.6) @@ -1174,6 +1180,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@faker-js/faker@8.4.0: + resolution: {integrity: sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + dev: true + /@humanwhocodes/config-array@0.11.13: resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} @@ -3719,6 +3730,14 @@ packages: ajv: 8.12.0 dev: false + /express-query-parser@1.3.3: + resolution: {integrity: sha512-sL4G5D3e1WVNQv28KmwWVpBPtUaqdhZV62nPNe409terbS4RPvTHCHqEKgYH4uyxeBQWcK0kYJ2KABxOt0GbDA==} + dependencies: + express: 4.18.2 + transitivePeerDependencies: + - supports-color + dev: false + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} diff --git a/backend/node/src/adapters/controllers/create-user.controller.spec.ts b/backend/node/src/adapters/controllers/create-user.controller.spec.ts index e85cfe0..d28797a 100644 --- a/backend/node/src/adapters/controllers/create-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/create-user.controller.spec.ts @@ -23,6 +23,7 @@ describe('createUserController', () => { }; const dto: CreateUserDto = { + id: 'id', first_name: 'John', last_name: 'Doe', email: 'johndoe@example.com', @@ -69,7 +70,7 @@ describe('createUserController', () => { const data = await controller(request); - expect>(data).toEqual({ + expect>(data).toStrictEqual({ status: StatusCodes.CREATED, data: { ...user, diff --git a/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts b/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts index 9fc08c3..e17c842 100644 --- a/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts +++ b/backend/node/src/adapters/controllers/find-all-users.controller.spec.ts @@ -1,12 +1,16 @@ import { StatusCodes } from 'http-status-codes'; import { mockedFindAllUsers } from '../../../__tests__/mocks/user.mock'; import type { FileService } from '../../core/interfaces/file.interface'; +import { PaginationResult } from '../../core/interfaces/http.interface'; import type { UserRepository, UserResult, UserTable, } from '../../core/interfaces/user.interface'; -import type { HttpRequest } from '../interfaces/http.interface'; +import type { + HttpRequest, + JsonApiPagination, +} from '../interfaces/http.interface'; import { findAllUsersController } from './find-all-users.controller'; describe('findAllUsersController', () => { @@ -27,13 +31,26 @@ describe('findAllUsersController', () => { const controller = findAllUsersController(userRepository, fileService); - const request: HttpRequest = { + const request: HttpRequest = { headers: {}, - params: {}, + params: { + page: { + size: 10, + number: 1, + }, + }, body: {}, user: {}, cookies: {}, }; + const result: PaginationResult = { + data: [], + meta: { + page: Number(request.params?.page.number), + limit: Number(request.params?.page.size), + total: 1, + }, + }; let user: UserTable = { id: 'id', @@ -47,28 +64,38 @@ describe('findAllUsersController', () => { beforeEach(() => { mockedFindAllUsers.mockImplementation(() => { - return Promise.resolve([ - { - ...user, - avatar: user.photo ? 'avatar.png' : null, - }, - ]); + return Promise.resolve>({ + ...result, + data: [ + { + ...user, + avatar: user.photo ? 'avatar.png' : null, + }, + ], + }); }); }); test('should return empty array', async () => { - mockedFindAllUsers.mockReturnValue(Promise.resolve([])); + mockedFindAllUsers.mockReturnValue( + Promise.resolve>(result) + ); - const data = await controller(request); + const { status, data, meta } = await controller(request); - expect(data.status).toEqual(StatusCodes.OK); - expect(data.data).toEqual([]); + expect(status).toStrictEqual(StatusCodes.OK); + expect(data).toStrictEqual([]); + expect(meta).toStrictEqual(result.meta); + expect(meta?.page).toStrictEqual(result.meta.page); + expect(meta?.limit).toStrictEqual(result.meta.limit); + expect(meta?.total).toStrictEqual(result.meta.total); }); test('should return all users', async () => { - const data = await controller(request); - expect(data.status).toEqual(StatusCodes.OK); - expect(data.data).toEqual( + const { data, status, meta } = await controller(request); + + expect(status).toStrictEqual(StatusCodes.OK); + expect(data).toStrictEqual( expect.arrayContaining([ { ...user, @@ -76,6 +103,10 @@ describe('findAllUsersController', () => { }, ]) ); + expect(meta).toStrictEqual(result.meta); + expect(meta?.page).toStrictEqual(result.meta.page); + expect(meta?.limit).toStrictEqual(result.meta.limit); + expect(meta?.total).toStrictEqual(result.meta.total); }); test('should return all users with avatar', async () => { @@ -84,10 +115,10 @@ describe('findAllUsersController', () => { photo: 'photo.png', }; - const data = await controller(request); + const { data, status, meta } = await controller(request); - expect(data.status).toEqual(StatusCodes.OK); - expect(data.data).toEqual( + expect(status).toStrictEqual(StatusCodes.OK); + expect(data).toStrictEqual( expect.arrayContaining([ { ...user, @@ -95,5 +126,9 @@ describe('findAllUsersController', () => { }, ]) ); + expect(meta).toStrictEqual(result.meta); + expect(meta?.page).toStrictEqual(result.meta.page); + expect(meta?.limit).toStrictEqual(result.meta.limit); + expect(meta?.total).toStrictEqual(result.meta.total); }); }); diff --git a/backend/node/src/adapters/controllers/find-all-users.controller.ts b/backend/node/src/adapters/controllers/find-all-users.controller.ts index a5108b7..81df747 100644 --- a/backend/node/src/adapters/controllers/find-all-users.controller.ts +++ b/backend/node/src/adapters/controllers/find-all-users.controller.ts @@ -6,18 +6,29 @@ import type { } from '../../core/interfaces/user.interface'; import { findAllUsers } from '../../core/use-cases/find-all-users.use-case'; import type { ResponseModel } from '../interfaces/common.interface'; -import type { HttpRequest } from '../interfaces/http.interface'; +import type { + HttpRequest, + JsonApiPagination, +} from '../interfaces/http.interface'; export function findAllUsersController( userRepository: UserRepository, fileService: FileService -): (req: HttpRequest) => Promise> { - return async (_req: HttpRequest): Promise> => { - const data = await findAllUsers(userRepository, fileService); +): ( + req: HttpRequest +) => Promise> { + return async ( + req: HttpRequest + ): Promise> => { + const { data, meta } = await findAllUsers(userRepository, fileService, { + page: Number(req.params?.page.number), + limit: Number(req.params?.page.size), + }); return { status: StatusCodes.OK, data, + meta, }; }; } diff --git a/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts b/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts index da429f6..3e37ccf 100644 --- a/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/find-one-user.controller.spec.ts @@ -67,7 +67,9 @@ describe('findOneUserController', () => { const data = await controller(request); - expect>(data).toEqual({ + expect>(data).toStrictEqual< + ResponseModel + >({ status: StatusCodes.OK, data: { ...user, @@ -84,7 +86,9 @@ describe('findOneUserController', () => { const data = await controller(request); - expect>(data).toEqual({ + expect>(data).toStrictEqual< + ResponseModel + >({ status: StatusCodes.OK, data: { ...user, diff --git a/backend/node/src/adapters/controllers/home.controller.spec.ts b/backend/node/src/adapters/controllers/home.controller.spec.ts index f725b0c..2a2bf39 100644 --- a/backend/node/src/adapters/controllers/home.controller.spec.ts +++ b/backend/node/src/adapters/controllers/home.controller.spec.ts @@ -9,7 +9,7 @@ describe('homeController', () => { test('should return hello world!', () => { const data = homeController(request); - expect>(data).toEqual({ + expect>(data).toStrictEqual>({ status: StatusCodes.OK, data: 'Hello world!', }); diff --git a/backend/node/src/adapters/controllers/remove-user.controller.spec.ts b/backend/node/src/adapters/controllers/remove-user.controller.spec.ts index fc76aba..f3b8184 100644 --- a/backend/node/src/adapters/controllers/remove-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/remove-user.controller.spec.ts @@ -51,9 +51,11 @@ describe('removeUserController', () => { const data = await controller(request); - expect(removeUser).toBeCalledTimes(1); - expect(removeUser).toBeCalledWith('id', userRepository, fileService); - expect(data).toEqual({ + expect(removeUser).toHaveBeenCalledTimes(1); + expect(removeUser).toHaveBeenCalledWith< + [string, UserRepository, FileService] + >('id', userRepository, fileService); + expect(data).toStrictEqual({ status: StatusCodes.NO_CONTENT, }); }); diff --git a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts index de3a593..da4d096 100644 --- a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts +++ b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts @@ -48,7 +48,7 @@ describe('uniqueUserEmailController', () => { throw new BadRequestException('The email has already been taken.'); } - return Promise.resolve(true); + return Promise.resolve(true); } ); }); @@ -56,7 +56,7 @@ describe('uniqueUserEmailController', () => { test('should only check when email is provided', async () => { const data = await controller(request); - expect(data).toEqual(true); + expect(data).toStrictEqual(true); }); test('should throw error when email is not unique', async () => { @@ -79,6 +79,6 @@ describe('uniqueUserEmailController', () => { const data = await controller(request); - expect(data).toEqual(true); + expect(data).toStrictEqual(true); }); }); diff --git a/backend/node/src/adapters/controllers/update-user.controller.spec.ts b/backend/node/src/adapters/controllers/update-user.controller.spec.ts index 8bfd8d1..746ee17 100644 --- a/backend/node/src/adapters/controllers/update-user.controller.spec.ts +++ b/backend/node/src/adapters/controllers/update-user.controller.spec.ts @@ -5,8 +5,10 @@ import type { FileService } from '../../core/interfaces/file.interface'; import type { UpdateUserDto, UserRepository, + UserResult, UserTable, } from '../../core/interfaces/user.interface'; +import { ResponseModel } from '../interfaces/common.interface'; import type { HttpRequest } from '../interfaces/http.interface'; import type { UserRequestParams } from '../interfaces/user.interface'; import { updateUserController } from './update-user.controller'; @@ -51,7 +53,7 @@ describe('updateUserController', () => { beforeEach(() => { mockedUpdateUser.mockImplementation(() => { - return Promise.resolve({ + return Promise.resolve({ ...user, first_name: dto.first_name ?? user.first_name, last_name: dto.last_name ?? user.last_name, @@ -73,7 +75,9 @@ describe('updateUserController', () => { const data = await controller(request); - expect(data).toEqual({ + expect>(data).toStrictEqual< + ResponseModel + >({ status: StatusCodes.OK, data: { ...user, @@ -90,7 +94,9 @@ describe('updateUserController', () => { const data = await controller(request); - expect(data).toEqual({ + expect>(data).toStrictEqual< + ResponseModel + >({ status: StatusCodes.OK, data: { ...user, @@ -102,10 +108,14 @@ describe('updateUserController', () => { }); test("should only update user's email", async () => { - dto = { email: 'janedoe@example.com' }; + dto = { + email: 'janedoe@example.com', + }; const data = await controller(request); - expect(data).toEqual({ + expect>(data).toStrictEqual< + ResponseModel + >({ status: StatusCodes.OK, data: { ...user, @@ -117,11 +127,16 @@ describe('updateUserController', () => { test("should only update user's avatar", async () => { dto = {}; - user = { ...user, photo: 'photo.png' }; + user = { + ...user, + photo: 'photo.png', + }; const data = await controller(request); - expect(data).toEqual({ + expect>(data).toStrictEqual< + ResponseModel + >({ status: StatusCodes.OK, data: { ...user, diff --git a/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts b/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts index eee9c6b..442eb1b 100644 --- a/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts +++ b/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts @@ -37,6 +37,6 @@ describe('validateUuidController', () => { const data = validateUuidController(request); - expect(data).toEqual(true); + expect(data).toStrictEqual(true); }); }); diff --git a/backend/node/src/adapters/interfaces/common.interface.ts b/backend/node/src/adapters/interfaces/common.interface.ts index d10bca4..1f24f61 100644 --- a/backend/node/src/adapters/interfaces/common.interface.ts +++ b/backend/node/src/adapters/interfaces/common.interface.ts @@ -1,3 +1,4 @@ +import { PaginationResult } from '../../core/interfaces/http.interface'; import type { HttpRequest } from './http.interface'; export interface RequestIdParams { @@ -11,6 +12,7 @@ export type Controller = ( export interface ResponseModel> { status: number; data?: Model; + meta?: PaginationResult['meta']; cookie?: { name: string; value: string; diff --git a/backend/node/src/adapters/interfaces/http.interface.ts b/backend/node/src/adapters/interfaces/http.interface.ts index bbe7420..c2badfe 100644 --- a/backend/node/src/adapters/interfaces/http.interface.ts +++ b/backend/node/src/adapters/interfaces/http.interface.ts @@ -66,3 +66,10 @@ export interface JsonApiErrorObject { export interface JsonApiError extends JsonApi { errors: JsonApiErrorObject[]; } + +export interface JsonApiPagination { + page: { + number: number; + size: number; + }; +} diff --git a/backend/node/src/core/entities/validation.entity.ts b/backend/node/src/core/entities/validation.entity.ts new file mode 100644 index 0000000..5be9789 --- /dev/null +++ b/backend/node/src/core/entities/validation.entity.ts @@ -0,0 +1,65 @@ +interface ErrorSchemaError { + 'page.required': string; + 'page.type': string; + 'page.number.required': string; + 'page.size.required': string; + 'page.number.type': string; + 'page.size.type': string; + 'data.required': string; + 'jsonapi.version.required': string; + 'jsonapi.version.type': string; + 'links.self.required': string; + 'links.self.type': string; + 'data.type.required': string; + 'data.id.format': string; + 'data.type.pattern': string; + 'data.attributes.first_name.required': string; + 'data.attributes.email.required': string; + 'data.attributes.email.minLength': string; + 'data.attributes.email.format': string; + 'data.attributes.first_name.type': string; + 'data.attributes.first_name.minLength': string; + 'data.attributes.email.type': string; + 'email.unique': string; + 'id.required': string; + 'id.format': string; + 'user.exist': string; +} + +const errorMessage: ErrorSchemaError = { + 'page.required': 'The page property is required.', + 'page.type': 'The page property must be an object.', + 'page.number.required': 'The page.number property is required.', + 'page.size.required': 'The page.size property is required.', + 'page.number.type': 'The page.number property must be an integer.', + 'page.size.type': 'The page.size property must be an integer.', + 'data.required': 'The data property is required.', + 'jsonapi.version.required': 'The jsonapi.version property is required.', + 'jsonapi.version.type': 'The jsonapi.version property must be a string.', + 'links.self.required': 'The links.self property is required.', + 'links.self.type': 'The links.self property must be a string.', + 'data.type.required': 'The data.type property is required.', + 'data.id.format': 'The data.id property must be a UUID (version 4).', + 'data.type.pattern': `The data.type property must be 'users'`, + 'data.attributes.first_name.required': + 'The data.attributes.first_name property is required.', + 'data.attributes.email.required': + 'The data.attributes.email property is required.', + 'data.attributes.email.minLength': + 'The data.attributes.email property is required.', + 'data.attributes.email.format': + 'The data.attributes.email property must be a valid email address.', + 'data.attributes.first_name.type': + 'The data.attributes.first_name must be a string.', + 'data.attributes.first_name.minLength': + 'The data.attributes.first_name field must have a value.', + 'data.attributes.email.type': 'The data.attributes.email must be a string.', + 'email.unique': 'The email has already been taken.', + 'id.required': 'Validation failed (uuid v4 is expected).', + 'id.format': 'Validation failed (uuid v4 is expected).', + 'user.exist': 'The user is not found.', +}; + +export function getErrorMessage(key: keyof ErrorSchemaError): string { + return errorMessage[key]; +} diff --git a/backend/node/src/core/interfaces/common.interface.ts b/backend/node/src/core/interfaces/common.interface.ts index c1c4032..709e5a4 100644 --- a/backend/node/src/core/interfaces/common.interface.ts +++ b/backend/node/src/core/interfaces/common.interface.ts @@ -1,6 +1,8 @@ +import { PaginationParams, PaginationResult } from './http.interface'; + export interface BaseRepository { create: (data: TableInput) => Promise
; - findAll: () => Promise; + findAll: (options: PaginationParams) => Promise>; findOne: (id: string) => Promise
; update: (id: string, data: Partial) => Promise
; remove: (id: string) => Promise
; diff --git a/backend/node/src/core/interfaces/http.interface.ts b/backend/node/src/core/interfaces/http.interface.ts index 1b3fbce..4656044 100644 --- a/backend/node/src/core/interfaces/http.interface.ts +++ b/backend/node/src/core/interfaces/http.interface.ts @@ -7,3 +7,15 @@ export interface HttpError extends Error { statusCode: number; error?: ErrorObject | Record; } + +export interface PaginationParams { + page: number; + limit: number; +} + +export interface PaginationResult> { + data: Entity[]; + meta: PaginationParams & { + total: number; + }; +} diff --git a/backend/node/src/core/interfaces/user.interface.ts b/backend/node/src/core/interfaces/user.interface.ts index 73b4518..ebc8dd3 100644 --- a/backend/node/src/core/interfaces/user.interface.ts +++ b/backend/node/src/core/interfaces/user.interface.ts @@ -3,7 +3,7 @@ import type { BaseRepository } from './common.interface'; export interface CreateUserDto extends Omit { - id?: string; + id: string; last_name: string | null; nickname?: string; } diff --git a/backend/node/src/core/use-cases/create-user.use-case.spec.ts b/backend/node/src/core/use-cases/create-user.use-case.spec.ts index 8649331..bde9d19 100644 --- a/backend/node/src/core/use-cases/create-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/create-user.use-case.spec.ts @@ -11,6 +11,7 @@ describe('createUser', () => { let userRepository: UserRepository; const dto: CreateUserDto = { + id: 'id', first_name: 'Doe', last_name: 'Doe', email: 'johndoe@example.com', @@ -44,7 +45,9 @@ describe('createUser', () => { }); test('should throw error when user is not saved', async () => { - userRepository.create = jest.fn().mockReturnValue(Promise.resolve(null)); + userRepository.create = jest + .fn() + .mockReturnValue(Promise.resolve(null)); await expect(createUser(dto, userRepository)).rejects.toThrow( new Error('Something went wrong.') @@ -54,8 +57,10 @@ describe('createUser', () => { test('should create an user', async () => { const data = await createUser(dto, userRepository); - expect(userRepository.create).toBeCalledTimes(1); - expect(data).toEqual({ + expect( + userRepository.create + ).toHaveBeenCalledTimes(1); + expect(data).toStrictEqual({ ...user, avatar: null, }); diff --git a/backend/node/src/core/use-cases/create-user.use-case.ts b/backend/node/src/core/use-cases/create-user.use-case.ts index af8c1c1..ee9a677 100644 --- a/backend/node/src/core/use-cases/create-user.use-case.ts +++ b/backend/node/src/core/use-cases/create-user.use-case.ts @@ -11,16 +11,16 @@ import type { * * @param {CreateUserDto} dto Validated data transfer object * @param {UserRepository} userRepository A repository of user - * @param {HashService} hashService A service for manage hash * @return {Promise} An user entity */ export async function createUser( dto: CreateUserDto, userRepository: UserRepository ): Promise { - const { first_name: firstName, last_name: lastName, email } = dto; + const { id, first_name: firstName, last_name: lastName, email } = dto; const record = await userRepository.create({ + id, first_name: firstName, last_name: lastName, email, diff --git a/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts b/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts index 087ded8..55cbd9c 100644 --- a/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts +++ b/backend/node/src/core/use-cases/find-all-users.use-case.spec.ts @@ -1,11 +1,31 @@ import type { FileService } from '../interfaces/file.interface'; -import type { UserRepository, UserTable } from '../interfaces/user.interface'; +import { + PaginationParams, + PaginationResult, +} from '../interfaces/http.interface'; +import type { + UserRepository, + UserResult, + UserTable, +} from '../interfaces/user.interface'; import { findAllUsers } from './find-all-users.use-case'; describe('findAllUsers', () => { let userRepository: UserRepository; let fileService: FileService; + const params: PaginationParams = { + limit: 10, + page: 1, + }; + const result: PaginationResult = { + data: [], + meta: { + ...params, + total: 1, + }, + }; + const user: UserTable = { id: 'id', first_name: 'John', @@ -19,7 +39,12 @@ describe('findAllUsers', () => { beforeEach(() => { userRepository = { create: jest.fn(), - findAll: jest.fn().mockReturnValue(Promise.resolve([user])), + findAll: jest.fn().mockReturnValue( + Promise.resolve>({ + ...result, + data: [user], + }) + ), findOne: jest.fn(), findOneByEmail: jest.fn(), update: jest.fn(), @@ -29,26 +54,47 @@ describe('findAllUsers', () => { fileService = { upload: jest.fn(), - getUrl: jest.fn().mockReturnValue(Promise.resolve('avatar.png')), + getUrl: jest.fn().mockReturnValue(Promise.resolve('avatar.png')), remove: jest.fn(), }; }); test('should return empty array', async () => { - userRepository.findAll = jest.fn().mockReturnValue(Promise.resolve([])); + userRepository.findAll = jest + .fn() + .mockReturnValue(Promise.resolve>(result)); - const data = await findAllUsers(userRepository, fileService); + const { data, meta } = await findAllUsers( + userRepository, + fileService, + params + ); - expect(userRepository.findAll).toBeCalledTimes(1); - expect(fileService.getUrl).toBeCalledTimes(0); - expect(data).toEqual(expect.arrayContaining([])); + expect( + userRepository.findAll + ).toHaveBeenCalledTimes(1); + expect(fileService.getUrl).toHaveBeenCalledTimes(0); + expect(data).toStrictEqual(expect.arrayContaining([])); + expect(meta).toStrictEqual< + PaginationResult['meta'] + >(result.meta); + expect(meta.page).toStrictEqual(result.meta.page); + expect(meta.limit).toStrictEqual(result.meta.limit); + expect(meta.total).toStrictEqual(result.meta.total); }); test('should return all users', async () => { - const data = await findAllUsers(userRepository, fileService); - expect(userRepository.findAll).toBeCalledTimes(1); - expect(fileService.getUrl).toBeCalledTimes(0); - expect(data).toEqual( + const { data, meta } = await findAllUsers( + userRepository, + fileService, + params + ); + + expect( + userRepository.findAll + ).toHaveBeenCalledTimes(1); + expect(fileService.getUrl).toHaveBeenCalledTimes(0); + expect(data).toStrictEqual( expect.arrayContaining([ { ...user, @@ -56,17 +102,38 @@ describe('findAllUsers', () => { }, ]) ); + expect(meta).toStrictEqual< + PaginationResult['meta'] + >(result.meta); + expect(meta.page).toStrictEqual(result.meta.page); + expect(meta.limit).toStrictEqual(result.meta.limit); + expect(meta.total).toStrictEqual(result.meta.total); }); test('should return all users with avatar', async () => { - userRepository.findAll = jest - .fn() - .mockReturnValue(Promise.resolve([{ ...user, photo: 'photo.png' }])); + userRepository.findAll = jest.fn().mockReturnValue( + Promise.resolve>({ + ...result, + data: [ + { + ...user, + photo: 'photo.png', + }, + ], + }) + ); + + const { data, meta } = await findAllUsers( + userRepository, + fileService, + params + ); - const data = await findAllUsers(userRepository, fileService); - expect(userRepository.findAll).toBeCalledTimes(1); - expect(fileService.getUrl).toBeCalledTimes(1); - expect(data).toEqual( + expect( + userRepository.findAll + ).toHaveBeenCalledTimes(1); + expect(fileService.getUrl).toHaveBeenCalledTimes(1); + expect(data).toStrictEqual( expect.arrayContaining([ { ...user, @@ -75,5 +142,11 @@ describe('findAllUsers', () => { }, ]) ); + expect(meta).toStrictEqual< + PaginationResult['meta'] + >(result.meta); + expect(meta.page).toStrictEqual(result.meta.page); + expect(meta.limit).toStrictEqual(result.meta.limit); + expect(meta.total).toStrictEqual(result.meta.total); }); }); diff --git a/backend/node/src/core/use-cases/find-all-users.use-case.ts b/backend/node/src/core/use-cases/find-all-users.use-case.ts index eb4705b..618f65f 100644 --- a/backend/node/src/core/use-cases/find-all-users.use-case.ts +++ b/backend/node/src/core/use-cases/find-all-users.use-case.ts @@ -1,4 +1,8 @@ import type { FileService } from '../interfaces/file.interface'; +import { + PaginationParams, + PaginationResult, +} from '../interfaces/http.interface'; import type { UserRepository, UserResult } from '../interfaces/user.interface'; /** @@ -10,11 +14,12 @@ import type { UserRepository, UserResult } from '../interfaces/user.interface'; */ export async function findAllUsers( userRepository: UserRepository, - fileService: FileService -): Promise { - const records = await userRepository.findAll(); + fileService: FileService, + params: PaginationParams +): Promise> { + const { data: records, meta } = await userRepository.findAll(params); - const result = await Promise.all( + const data = await Promise.all( records.map(async (obj) => { let avatar: string | null = null; if (obj.photo) { @@ -29,8 +34,14 @@ export async function findAllUsers( ); if (!records.length) { - return []; + return { + data: [], + meta, + }; } - return result; + return { + data, + meta, + }; } diff --git a/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts b/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts index 19bef86..100414a 100644 --- a/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/find-one-user.use-case.spec.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../entities/validation.entity'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { @@ -40,7 +41,7 @@ describe('findOneUser', () => { fileService = { upload: jest.fn(), - getUrl: jest.fn().mockReturnValue(Promise.resolve('avatar.png')), + getUrl: jest.fn().mockReturnValue(Promise.resolve('avatar.png')), remove: jest.fn(), }; }); @@ -48,14 +49,17 @@ describe('findOneUser', () => { test('should throw error when user not found', async () => { await expect( findOneUser('invalid-id', userRepository, fileService) - ).rejects.toThrow(new NotFoundException('The user is not found.')); + ).rejects.toThrow(new NotFoundException(getErrorMessage('user.exist'))); }); test('should return an user without avatar', async () => { const data = await findOneUser(user.id, userRepository, fileService); - expect(userRepository.findOne).toBeCalledTimes(1); - expect(fileService.getUrl).toBeCalledTimes(0); - expect(data).toEqual({ + + expect( + userRepository.findOne + ).toHaveBeenCalledTimes(1); + expect(fileService.getUrl).toHaveBeenCalledTimes(0); + expect(data).toStrictEqual({ ...user, avatar: null, }); @@ -70,9 +74,12 @@ describe('findOneUser', () => { ); const data = await findOneUser(user.id, userRepository, fileService); - expect(userRepository.findOne).toBeCalledTimes(1); - expect(fileService.getUrl).toBeCalledTimes(1); - expect(data).toEqual({ + + expect( + userRepository.findOne + ).toHaveBeenCalledTimes(1); + expect(fileService.getUrl).toHaveBeenCalledTimes(1); + expect(data).toStrictEqual({ ...user, photo: 'photo.png', avatar: 'avatar.png', diff --git a/backend/node/src/core/use-cases/find-one-user.use-case.ts b/backend/node/src/core/use-cases/find-one-user.use-case.ts index 589a63f..3dce006 100644 --- a/backend/node/src/core/use-cases/find-one-user.use-case.ts +++ b/backend/node/src/core/use-cases/find-one-user.use-case.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../entities/validation.entity'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { UserRepository, UserResult } from '../interfaces/user.interface'; @@ -20,7 +21,7 @@ export async function findOneUser( const record = await userRepository.findOne(id); if (!record) { - throw new NotFoundException('The user is not found.'); + throw new NotFoundException(getErrorMessage('user.exist')); } let avatar: string | null = null; diff --git a/backend/node/src/core/use-cases/remove-user.use-case.spec.ts b/backend/node/src/core/use-cases/remove-user.use-case.spec.ts index ff20ac3..22b44c8 100644 --- a/backend/node/src/core/use-cases/remove-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/remove-user.use-case.spec.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../entities/validation.entity'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { UserRepository, UserTable } from '../interfaces/user.interface'; @@ -26,7 +27,7 @@ describe('removeUser', () => { update: jest.fn(), remove: jest.fn((id: string) => { if (id !== user.id) { - return Promise.resolve(null); + return Promise.resolve(null); } return Promise.resolve(user); @@ -44,14 +45,16 @@ describe('removeUser', () => { test('should throw error when user not found', async () => { await expect( removeUser('invalid-id', userRepository, fileService) - ).rejects.toThrow(new NotFoundException('The user is not found.')); + ).rejects.toThrow(new NotFoundException(getErrorMessage('user.exist'))); }); test('should delete user with no avatar', async () => { await removeUser('id', userRepository, fileService); - expect(userRepository.remove).toBeCalledTimes(1); - expect(fileService.remove).toBeCalledTimes(0); + expect( + userRepository.remove + ).toHaveBeenCalledTimes(1); + expect(fileService.remove).toHaveBeenCalledTimes(0); }); test('should delete user with an avatar', async () => { @@ -64,7 +67,9 @@ describe('removeUser', () => { await removeUser('id', userRepository, fileService); - expect(userRepository.remove).toBeCalledTimes(1); - expect(fileService.remove).toBeCalledTimes(1); + expect( + userRepository.remove + ).toHaveBeenCalledTimes(1); + expect(fileService.remove).toHaveBeenCalledTimes(1); }); }); diff --git a/backend/node/src/core/use-cases/remove-user.use-case.ts b/backend/node/src/core/use-cases/remove-user.use-case.ts index 7aba690..f8317e8 100644 --- a/backend/node/src/core/use-cases/remove-user.use-case.ts +++ b/backend/node/src/core/use-cases/remove-user.use-case.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../entities/validation.entity'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { UserRepository } from '../interfaces/user.interface'; @@ -20,7 +21,7 @@ export async function removeUser( const record = await userRepository.remove(id); if (!record) { - throw new NotFoundException('The user is not found.'); + throw new NotFoundException(getErrorMessage('user.exist')); } if (record.photo) { diff --git a/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts b/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts index 879f589..1cf1249 100644 --- a/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts +++ b/backend/node/src/core/use-cases/unique-user-email.use-case.spec.ts @@ -22,10 +22,10 @@ describe('uniqueUserEmail', () => { findOne: jest.fn(), findOneByEmail: jest.fn((email: string) => { if (email === user.email) { - return Promise.resolve(user); + return Promise.resolve(user); } - return Promise.resolve(null); + return Promise.resolve(null); }), update: jest.fn(), remove: jest.fn(), @@ -44,6 +44,6 @@ describe('uniqueUserEmail', () => { test('should return true when user is not found', async () => { const data = await uniqueUserEmail(userRepository, 'johndoe@example.co'); - expect(data).toEqual(true); + expect(data).toStrictEqual(true); }); }); diff --git a/backend/node/src/core/use-cases/unique-user-email.use-case.ts b/backend/node/src/core/use-cases/unique-user-email.use-case.ts index 36d6534..cdd1351 100644 --- a/backend/node/src/core/use-cases/unique-user-email.use-case.ts +++ b/backend/node/src/core/use-cases/unique-user-email.use-case.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../entities/validation.entity'; import { BadRequestException } from '../exceptions/bad-request.exception'; import type { UserRepository } from '../interfaces/user.interface'; @@ -9,7 +10,7 @@ export async function uniqueUserEmail( const isExists = await userRepository.findOneByEmail(email, userId); if (isExists) { - throw new BadRequestException('The email has already been taken.'); + throw new BadRequestException(getErrorMessage('email.unique')); } return true; diff --git a/backend/node/src/core/use-cases/update-user.use-case.spec.ts b/backend/node/src/core/use-cases/update-user.use-case.spec.ts index 2c69b63..49c6983 100644 --- a/backend/node/src/core/use-cases/update-user.use-case.spec.ts +++ b/backend/node/src/core/use-cases/update-user.use-case.spec.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../entities/validation.entity'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { @@ -47,19 +48,19 @@ describe('updateUser', () => { upload: jest.fn(), getUrl: jest.fn().mockImplementation((_path: string) => { if (!user.photo) { - return Promise.resolve(null); + return Promise.resolve(null); } - return Promise.resolve('avatar.png'); + return Promise.resolve('avatar.png'); }), remove: jest.fn(), }; }); test('should throw error when user not found', async () => { - await expect( + await expect>( updateUser('invalid-id', dto, userRepository, fileService) - ).rejects.toThrow(new NotFoundException('The user is not found.')); + ).rejects.toThrow(new NotFoundException(getErrorMessage('user.exist'))); }); test("should only update user's name", async () => { @@ -70,7 +71,7 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ + expect(data).toStrictEqual({ ...user, ...dto, avatar: null, @@ -84,7 +85,7 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ + expect(data).toStrictEqual({ ...user, ...dto, avatar: null, @@ -98,7 +99,7 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ + expect(data).toStrictEqual({ ...user, ...dto, avatar: null, @@ -114,7 +115,7 @@ describe('updateUser', () => { const data = await updateUser('id', dto, userRepository, fileService); - expect(data).toEqual({ + expect(data).toStrictEqual({ ...user, photo: 'photo.png', avatar: 'avatar.png', diff --git a/backend/node/src/core/use-cases/update-user.use-case.ts b/backend/node/src/core/use-cases/update-user.use-case.ts index b2f21b3..856f783 100644 --- a/backend/node/src/core/use-cases/update-user.use-case.ts +++ b/backend/node/src/core/use-cases/update-user.use-case.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../entities/validation.entity'; import { NotFoundException } from '../exceptions/not-found.exception'; import type { FileService } from '../interfaces/file.interface'; import type { @@ -47,7 +48,7 @@ export async function updateUser( const record = await userRepository.update(id, input); if (!record) { - throw new NotFoundException('The user is not found.'); + throw new NotFoundException(getErrorMessage('user.exist')); } let avatar: string | null = null; diff --git a/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts b/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts index 80eebdb..450c1da 100644 --- a/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts +++ b/backend/node/src/core/use-cases/validate-uuid.use-case.spec.ts @@ -13,6 +13,8 @@ describe('validateUuid', () => { }); test('should return true if uuid is valid', () => { - expect(validateUuid(uuid)).toEqual(true); + expect>( + validateUuid(uuid) + ).toStrictEqual(true); }); }); diff --git a/backend/node/src/infrastructure/repositories/user.repository.ts b/backend/node/src/infrastructure/repositories/user.repository.ts index 07808e7..c6a7d2d 100644 --- a/backend/node/src/infrastructure/repositories/user.repository.ts +++ b/backend/node/src/infrastructure/repositories/user.repository.ts @@ -1,3 +1,4 @@ +import { PaginationParams } from '../../core/interfaces/http.interface'; import type { UserRepository, UserTable, @@ -29,21 +30,34 @@ export const userRepository: UserRepository = { return result; }, - findAll: async () => { - const records = await db('users').select( - 'id', - 'first_name', - 'last_name', - 'nickname', - 'email', - 'photo', - 'created_at', - 'updated_at' - ); - - const result = records; - - return result; + findAll: async (options: PaginationParams) => { + const offset = options.page > 1 ? (options.page - 1) * options.limit : 0; + + const [records, count] = await Promise.all([ + db('users') + .select( + 'id', + 'first_name', + 'last_name', + 'nickname', + 'email', + 'photo', + 'created_at', + 'updated_at' + ) + .limit(options.limit) + .offset(offset), + db('users').count>(), + ]); + + return { + data: records, + meta: { + page: options.page, + limit: options.limit, + total: Number(count?.at(0)?.count), + }, + }; }, findOne: async (id: string) => { diff --git a/backend/node/src/infrastructure/server/express/app.ts b/backend/node/src/infrastructure/server/express/app.ts index 0d26ab6..0406155 100644 --- a/backend/node/src/infrastructure/server/express/app.ts +++ b/backend/node/src/infrastructure/server/express/app.ts @@ -1,10 +1,21 @@ import express, { json } from 'express'; +import { queryParser } from 'express-query-parser'; import { errorHandler, notFoundHandler } from './middlewares/error'; import { errorLogger, httpLogger } from './middlewares/logger'; import { router } from './routes'; const app = express(); +// Query parser +app.use( + queryParser({ + parseNull: true, + parseUndefined: true, + parseBoolean: true, + parseNumber: true, + }) +); + // JSON parser app.use( json({ diff --git a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts index 8390c33..34a3e67 100644 --- a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts +++ b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express'; import { createReadStream } from 'fs'; import { extname } from 'path'; -import { DataDocument, Linker, Serializer } from 'ts-japi'; +import { DataDocument, Linker, Paginator, Serializer } from 'ts-japi'; import { createUserController } from '../../../../adapters/controllers/create-user.controller'; import { findAllUsersController } from '../../../../adapters/controllers/find-all-users.controller'; import { findOneUserController } from '../../../../adapters/controllers/find-one-user.controller'; @@ -11,6 +11,7 @@ import type { HttpRequest, HttpRequestBody, JsonApiData, + JsonApiPagination, } from '../../../../adapters/interfaces/http.interface'; import type { CreateUser, @@ -22,18 +23,14 @@ import { appConfig } from '../../../config/app'; import { userRepository } from '../../../repositories/user.repository'; import { fileService } from '../../../services/file.service'; -const userSerializer = new Serializer('users', { +const userPath = `${appConfig.url}/users`; + +const userSerializer = new Serializer('users', { version: '1.1', }); -const userLinker = (type: 'multi' | 'single' = 'multi') => { - const UserLinker = new Linker<[UserData | UserData[]]>((users) => { - return type === 'multi' || Array.isArray(users) - ? `${appConfig.url}/users/` - : `${appConfig.url}/users/${users.id}`; - }); - - return UserLinker; -}; +const UserLinker = new Linker<[UserData]>((users) => { + return `${userPath}/${users.id}`; +}); export async function create( req: Request>, @@ -66,7 +63,7 @@ export async function create( const result = await userSerializer.serialize(data, { linkers: { - resource: userLinker('single'), + resource: UserLinker, }, }); @@ -74,15 +71,38 @@ export async function create( } export async function findAll( - _req: Request, + req: Request, res: Response>> ) { const controller = findAllUsersController(userRepository, fileService); - const { status, data } = await controller({}); + const { status, data, meta } = await controller({ + params: req.query, + }); + + const UserPaginator = new Paginator((_user) => { + const { + page: { size: limit, number: page }, + } = req.query; + const totalPages = Math.ceil(Number(meta?.total) / limit); + const prevPage = page > 1 ? page - 1 : 0; + const nextPage = page < totalPages ? page + 1 : 0; + + return { + first: `${userPath}/?page[number]=1&page[size]=${req.query.page.size}`, + last: `${userPath}/?page[number]=${totalPages}&page[size]=${req.query.page.size}`, + prev: prevPage + ? `${userPath}/?page[number]=${prevPage}&page[size]=${req.query.page.size}` + : null, + next: nextPage + ? `${userPath}/?page[number]=${nextPage}&page[size]=${req.query.page.size}` + : null, + }; + }); const result = await userSerializer.serialize(data, { linkers: { - resource: userLinker('multi'), + resource: UserLinker, + paginator: UserPaginator, }, }); @@ -101,7 +121,7 @@ export async function findOne( const result = await userSerializer.serialize(data, { linkers: { - resource: userLinker('single'), + resource: UserLinker, }, }); @@ -141,7 +161,7 @@ export async function update( const result = await userSerializer.serialize(data, { linkers: { - resource: userLinker('single'), + resource: UserLinker, }, }); diff --git a/backend/node/src/infrastructure/server/express/middlewares/validator.ts b/backend/node/src/infrastructure/server/express/middlewares/validator.ts index 3896b19..d37f48e 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/validator.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/validator.ts @@ -21,12 +21,20 @@ ajvErrors(ajv); export function validate(schema: AllowedSchema, key: OptionKey = 'body') { switch (key) { case 'params': - return validator.validate({ params: schema }); + return validator.validate({ + params: schema, + }); case 'query': - return validator.validate({ query: schema }); + return validator.validate({ + query: schema, + }); case 'body': - return validator.validate({ body: schema }); + return validator.validate({ + body: schema, + }); default: - return validator.validate({ body: schema }); + return validator.validate({ + body: schema, + }); } } diff --git a/backend/node/src/infrastructure/server/express/routes/user.route.ts b/backend/node/src/infrastructure/server/express/routes/user.route.ts index 3c104f4..b86d4cd 100644 --- a/backend/node/src/infrastructure/server/express/routes/user.route.ts +++ b/backend/node/src/infrastructure/server/express/routes/user.route.ts @@ -10,11 +10,19 @@ import { import { uniqueUserEmail } from '../middlewares/unique-user-email'; import { validate } from '../middlewares/validator'; import { idParamSchema } from '../schemas/common.schema'; -import { createUserSchema, updateUserSchema } from '../schemas/user.schema'; +import { + createUserSchema, + findAllUserSchema, + updateUserSchema, +} from '../schemas/user.schema'; const userRouter = Router(); -userRouter.get('/', asyncHandler(findAll)); +userRouter.get( + '/', + [validate(findAllUserSchema, 'query')], + asyncHandler(findAll) +); userRouter.post( '/', diff --git a/backend/node/src/infrastructure/server/express/schemas/auth.schema.ts b/backend/node/src/infrastructure/server/express/schemas/auth.schema.ts deleted file mode 100644 index 130d131..0000000 --- a/backend/node/src/infrastructure/server/express/schemas/auth.schema.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { AllowedSchema } from 'express-json-validator-middleware'; - -export const logInSchema: AllowedSchema = { - type: 'object', - properties: { - username: { - type: 'string', - minLength: 1, - }, - password: { - type: 'string', - minLength: 1, - }, - }, - required: ['username', 'password'], - errorMessage: { - required: { - username: 'The username is required.', - password: 'The password is required.', - }, - properties: { - username: 'The username is required.', - password: 'The password is required.', - }, - }, -}; diff --git a/backend/node/src/infrastructure/server/express/schemas/common.schema.ts b/backend/node/src/infrastructure/server/express/schemas/common.schema.ts index 206bfd8..9463763 100644 --- a/backend/node/src/infrastructure/server/express/schemas/common.schema.ts +++ b/backend/node/src/infrastructure/server/express/schemas/common.schema.ts @@ -1,4 +1,5 @@ import type { AllowedSchema } from 'express-json-validator-middleware'; +import { getErrorMessage } from '../../../../core/entities/validation.entity'; export const idParamSchema: AllowedSchema = { type: 'object', @@ -6,12 +7,15 @@ export const idParamSchema: AllowedSchema = { id: { type: 'string', format: 'uuid', + errorMessage: { + format: getErrorMessage('id.format'), + }, }, }, required: ['id'], errorMessage: { - properties: { - id: 'Validation failed (uuid v4 is expected).', + required: { + id: getErrorMessage('id.required'), }, }, }; diff --git a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts index b13426a..23c22db 100644 --- a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts +++ b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts @@ -1,4 +1,42 @@ import type { AllowedSchema } from 'express-json-validator-middleware'; +import { getErrorMessage } from '../../../../core/entities/validation.entity'; + +export const findAllUserSchema: AllowedSchema = { + type: 'object', + properties: { + page: { + type: 'object', + properties: { + number: { + type: 'integer', + errorMessage: { + type: getErrorMessage('page.number.type'), + }, + }, + size: { + type: 'integer', + errorMessage: { + type: getErrorMessage('page.size.type'), + }, + }, + }, + required: ['number', 'size'], + errorMessage: { + required: { + number: getErrorMessage('page.number.required'), + size: getErrorMessage('page.size.required'), + }, + type: getErrorMessage('page.type'), + }, + }, + }, + required: ['page'], + errorMessage: { + required: { + page: getErrorMessage('page.required'), + }, + }, +}; export const createUserSchema: AllowedSchema = { type: 'object', @@ -9,14 +47,14 @@ export const createUserSchema: AllowedSchema = { version: { type: 'string', errorMessage: { - type: 'The JSON:API version must be a string.', + type: getErrorMessage('jsonapi.version.type'), }, }, }, required: ['version'], errorMessage: { required: { - version: 'The JSON:API version is required.', + version: getErrorMessage('jsonapi.version.required'), }, }, }, @@ -27,14 +65,14 @@ export const createUserSchema: AllowedSchema = { type: 'string', format: 'uuid', errorMessage: { - format: 'The data.id property must be a UUID (version 4).', + format: getErrorMessage('data.id.format'), }, }, type: { type: 'string', pattern: 'users', errorMessage: { - pattern: `The data.type property must be 'users'`, + pattern: getErrorMessage('data.type.pattern'), }, }, attributes: { @@ -53,22 +91,18 @@ export const createUserSchema: AllowedSchema = { minLength: 1, format: 'email', errorMessage: { - minLength: 'The data.attributes.email property is required.', - format: - 'The data.attributes.email property must be a valid email address.', + minLength: getErrorMessage('data.attributes.email.minLength'), + format: getErrorMessage('data.attributes.email.format'), }, }, }, required: ['first_name', 'email'], errorMessage: { required: { - first_name: - 'The data.attributes.first_name property is required.', - email: 'The data.attributes.email property is required.', - }, - properties: { - first_name: - 'The data.attributes.first_name property is required.', + first_name: getErrorMessage( + 'data.attributes.first_name.required' + ), + email: getErrorMessage('data.attributes.email.required'), }, }, }, @@ -76,7 +110,7 @@ export const createUserSchema: AllowedSchema = { required: ['type'], errorMessage: { required: { - type: 'The data.type property is required.', + type: getErrorMessage('data.type.required'), }, }, }, @@ -86,14 +120,14 @@ export const createUserSchema: AllowedSchema = { self: { type: 'string', errorMessage: { - type: 'The links.self property must be a string.', + type: getErrorMessage('links.self.type'), }, }, }, required: ['self'], errorMessage: { required: { - self: 'The links.self property is required.', + self: getErrorMessage('links.self.required'), }, }, }, @@ -101,7 +135,7 @@ export const createUserSchema: AllowedSchema = { required: ['data'], errorMessage: { required: { - data: 'The data property is required.', + data: getErrorMessage('data.required'), }, }, }; @@ -115,14 +149,14 @@ export const updateUserSchema: AllowedSchema = { version: { type: 'string', errorMessage: { - type: 'The JSON:API version must be a string.', + type: getErrorMessage('jsonapi.version.type'), }, }, }, required: ['version'], errorMessage: { required: { - version: 'The JSON:API version is required.', + version: getErrorMessage('jsonapi.version.required'), }, }, }, @@ -133,14 +167,14 @@ export const updateUserSchema: AllowedSchema = { type: 'string', format: 'uuid', errorMessage: { - format: 'The data.id property must be a UUID (version 4).', + format: getErrorMessage('data.id.format'), }, }, type: { type: 'string', pattern: 'users', errorMessage: { - pattern: `The data.type property must be 'users'`, + pattern: getErrorMessage('data.type.pattern'), }, }, attributes: { @@ -150,9 +184,10 @@ export const updateUserSchema: AllowedSchema = { type: 'string', minLength: 1, errorMessage: { - type: 'The data.attributes.first_name must be a string.', - minLength: - 'The data.attributes.first_name field must have a value.', + type: getErrorMessage('data.attributes.first_name.type'), + minLength: getErrorMessage( + 'data.attributes.first_name.minLength' + ), }, }, last_name: { @@ -167,9 +202,8 @@ export const updateUserSchema: AllowedSchema = { type: 'string', format: 'email', errorMessage: { - type: 'The data.attributes.email must be a string.', - format: - 'The data.attributes.email must be a valid email address.', + type: getErrorMessage('data.attributes.email.type'), + format: getErrorMessage('data.attributes.email.format'), }, }, }, @@ -178,7 +212,7 @@ export const updateUserSchema: AllowedSchema = { required: ['type'], errorMessage: { required: { - type: 'The data.type property is required.', + type: getErrorMessage('data.type.required'), }, }, }, @@ -188,14 +222,14 @@ export const updateUserSchema: AllowedSchema = { self: { type: 'string', errorMessage: { - type: 'The links.self property must be a string.', + type: getErrorMessage('links.self.type'), }, }, }, required: ['self'], errorMessage: { required: { - self: 'The links.self property is required.', + self: getErrorMessage('links.self.required'), }, }, }, @@ -204,7 +238,7 @@ export const updateUserSchema: AllowedSchema = { dependencies: {}, errorMessage: { required: { - data: 'The data property is required.', + data: getErrorMessage('data.required'), }, dependencies: {}, }, diff --git a/config/kratos/email-password/hooks/after-registration.jsonnet b/config/kratos/email-password/hooks/after-registration.jsonnet index 6b11409..ed90ed9 100644 --- a/config/kratos/email-password/hooks/after-registration.jsonnet +++ b/config/kratos/email-password/hooks/after-registration.jsonnet @@ -1,5 +1,11 @@ function(ctx) { - first_name: ctx.identity.traits.name.first, - last_name: ctx.identity.traits.name.last, - email: ctx.identity.traits.email, + data: { + id: ctx.identity.id, + type: "users", + attributes: { + first_name: ctx.identity.traits.name.first, + last_name: ctx.identity.traits.name.last, + email: ctx.identity.traits.email, + } + } } diff --git a/config/kratos/email-password/identity.schema.json b/config/kratos/email-password/identity.schema.json index 1a13787..2e0e69c 100644 --- a/config/kratos/email-password/identity.schema.json +++ b/config/kratos/email-password/identity.schema.json @@ -37,7 +37,8 @@ "title": "Last Name", "type": "string" } - } + }, + "required": ["first", "last"] } }, "required": [ diff --git a/config/kratos/email-password/kratos.yml b/config/kratos/email-password/kratos.yml index 4931258..3ef2630 100644 --- a/config/kratos/email-password/kratos.yml +++ b/config/kratos/email-password/kratos.yml @@ -66,16 +66,16 @@ selfservice: after: password: hooks: - - hook: session - - hook: show_verification_ui - hook: web_hook config: - url: http://backend:3000/user + url: http://backend:3000/users method: POST body: file:///etc/config/kratos/hooks/after-registration.jsonnet response: ignore: true parse: false + - hook: session + - hook: show_verification_ui log: level: debug diff --git a/config/oathkeeper/access-rules.yml b/config/oathkeeper/access-rules.yml index 1bf9751..4b7f835 100644 --- a/config/oathkeeper/access-rules.yml +++ b/config/oathkeeper/access-rules.yml @@ -28,7 +28,7 @@ preserve_host: true url: "http://backend:3000" match: - url: "http://127.0.0.1:4455/<{user,user/*}>" + url: "http://127.0.0.1:4455/<{users,users/*}>" methods: - GET - POST diff --git a/docker-compose-express.yml b/docker-compose-express.yml index db3b13a..a460809 100644 --- a/docker-compose-express.yml +++ b/docker-compose-express.yml @@ -10,6 +10,8 @@ services: dockerfile: ./express.Dockerfile volumes: - ./backend/node:/app/node + environment: + DB_HOST: postgres command: pnpm migrate:up restart: on-failure networks: @@ -24,6 +26,8 @@ services: dockerfile: ./express.Dockerfile volumes: - ./backend/node:/app/node + environment: + DB_HOST: postgres command: pnpm seed:run restart: on-failure networks: @@ -41,6 +45,8 @@ services: - 3000:3000 volumes: - ./backend/node:/app/node + environment: + DB_HOST: postgres restart: on-failure networks: - playground diff --git a/docker-compose-ory.yml b/docker-compose-ory.yml index 82ef18c..9e27c46 100644 --- a/docker-compose-ory.yml +++ b/docker-compose-ory.yml @@ -34,10 +34,6 @@ services: environment: - DSN=postgres://postgres:postgres@postgres-kratos:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 volumes: - - type: volume - source: kratos-sqlite - target: /var/lib/sqlite - read_only: false - type: bind source: ./config/kratos/email-password target: /etc/config/kratos @@ -60,10 +56,6 @@ services: - SERVE_PUBLIC_BASE_URL=http://127.0.0.1:4455/.ory/kratos/public/ command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - - type: volume - source: kratos-sqlite - target: /var/lib/sqlite - read_only: false - type: bind source: ./config/kratos/email-password target: /etc/config/kratos diff --git a/openapi.yaml b/openapi.yaml index 1c40a57..79c381a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -67,6 +67,24 @@ paths: - "user" summary: Get all users. operationId: findAll + parameters: + - in: query + name: page + required: true + style: deepObject + explode: true + schema: + type: object + properties: + number: + type: integer + example: 1 + size: + type: integer + example: 10 + required: + - number + - size responses: "200": description: Return all users. From bd259944397225b24396b0ffcb7d71914f7e6d65 Mon Sep 17 00:00:00 2001 From: Husen Date: Sun, 4 Feb 2024 23:00:48 +0700 Subject: [PATCH 5/8] chore: implement jsonapi error object Signed-off-by: Husen --- backend/node/__tests__/app.test.ts | 5 +- backend/node/__tests__/user.test.ts | 97 ++++--------------- .../unique-user-email.controller.spec.ts | 5 +- .../validate-uuid.controller.spec.ts | 5 +- .../src/adapters/interfaces/http.interface.ts | 37 ++----- .../server/express/middlewares/error.ts | 23 +++-- .../server/express/middlewares/logger.ts | 2 +- 7 files changed, 47 insertions(+), 127 deletions(-) diff --git a/backend/node/__tests__/app.test.ts b/backend/node/__tests__/app.test.ts index 2e66fe0..ad389e2 100644 --- a/backend/node/__tests__/app.test.ts +++ b/backend/node/__tests__/app.test.ts @@ -31,12 +31,9 @@ describe('GET /404', () => { jsonapi: { version: '1.1', }, - links: { - self: '/404', - }, errors: [ { - status: StatusCodes.NOT_FOUND, + status: StatusCodes.NOT_FOUND.toString(), title: ReasonPhrases.NOT_FOUND, detail: 'Resource not found.', }, diff --git a/backend/node/__tests__/user.test.ts b/backend/node/__tests__/user.test.ts index b29b1e2..eb4ef73 100644 --- a/backend/node/__tests__/user.test.ts +++ b/backend/node/__tests__/user.test.ts @@ -31,12 +31,9 @@ describe('POST /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.required'), }, @@ -66,12 +63,9 @@ describe('POST /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.email.format'), }, @@ -101,12 +95,9 @@ describe('POST /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('email.unique'), }, @@ -188,12 +179,9 @@ describe('GET /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('page.required'), }, @@ -216,12 +204,9 @@ describe('GET /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('page.type'), }, @@ -246,12 +231,9 @@ describe('GET /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('page.number.required'), }, @@ -276,12 +258,9 @@ describe('GET /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('page.size.required'), }, @@ -307,17 +286,14 @@ describe('GET /users', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('page.number.type'), }, { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('page.size.type'), }, @@ -392,12 +368,9 @@ describe('GET /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users/invalid-id', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('id.format'), }, @@ -417,12 +390,9 @@ describe('GET /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users/60677a98-a65e-4abc-831c-45dd76e8f990', - }, errors: [ { - status: StatusCodes.NOT_FOUND, + status: StatusCodes.NOT_FOUND.toString(), title: ReasonPhrases.NOT_FOUND, detail: getErrorMessage('user.exist'), }, @@ -493,12 +463,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users/invalid-id', - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('id.format'), }, @@ -523,12 +490,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: '/users/60677a98-a65e-4abc-831c-45dd76e8f990', - }, errors: [ { - status: StatusCodes.NOT_FOUND, + status: StatusCodes.NOT_FOUND.toString(), title: ReasonPhrases.NOT_FOUND, detail: getErrorMessage('user.exist'), }, @@ -556,12 +520,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: `/users/${user.id}`, - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.first_name.type'), }, @@ -589,12 +550,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: `/users/${user.id}`, - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.first_name.minLength'), }, @@ -622,12 +580,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: `/users/${user.id}`, - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.first_name.type'), }, @@ -771,12 +726,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: `/users/${user.id}`, - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.email.type'), }, @@ -804,12 +756,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: `/users/${user.id}`, - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.email.format'), }, @@ -837,12 +786,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: `/users/${user.id}`, - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.email.type'), }, @@ -870,12 +816,9 @@ describe('PATCH /users/{id}', () => { jsonapi: { version: '1.1', }, - links: { - self: `/users/${user.id}`, - }, errors: [ { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: getErrorMessage('data.attributes.email.format'), }, diff --git a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts index da4d096..4f1fd2a 100644 --- a/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts +++ b/backend/node/src/adapters/controllers/unique-user-email.controller.spec.ts @@ -60,7 +60,10 @@ describe('uniqueUserEmailController', () => { }); test('should throw error when email is not unique', async () => { - request = { ...request, body: dto }; + request = { + ...request, + body: dto, + }; await expect(controller(request)).rejects.toThrow( new BadRequestException('The email has already been taken.') diff --git a/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts b/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts index 442eb1b..bc114fe 100644 --- a/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts +++ b/backend/node/src/adapters/controllers/validate-uuid.controller.spec.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '../../core/entities/validation.entity'; import { BadRequestException } from '../../core/exceptions/bad-request.exception'; import type { RequestIdParams } from '../interfaces/common.interface'; import type { HttpRequestParams } from '../interfaces/http.interface'; @@ -22,9 +23,7 @@ describe('validateUuidController', () => { expect(() => { validateUuidController(request); - }).toThrow( - new BadRequestException('Validation failed (uuid v4 is expected).') - ); + }).toThrow(new BadRequestException(getErrorMessage('id.format'))); }); test('should return true when id is valid uuid', () => { diff --git a/backend/node/src/adapters/interfaces/http.interface.ts b/backend/node/src/adapters/interfaces/http.interface.ts index c2badfe..3706e9f 100644 --- a/backend/node/src/adapters/interfaces/http.interface.ts +++ b/backend/node/src/adapters/interfaces/http.interface.ts @@ -1,3 +1,5 @@ +import { DataDocument, Dictionary, ErrorDocument, JapiError } from 'ts-japi'; +import Resource from 'ts-japi/lib/models/resource.model'; import type { File } from '../../core/entities/common.entity'; export interface HttpRequest< @@ -33,38 +35,15 @@ export type HttpRequestCookie> = HttpRequest< T >; -export interface JsonApi { - jsonapi: { - version: string; - }; - links: { - self: string; - }; -} - -export interface JsonApiDataObject> { - type: string; - id: string; - attributes: Attributes; -} - -export interface JsonApiData> - extends JsonApi { - data: JsonApiDataObject; -} - -export interface JsonApiErrorObject { - status: number; - title: string; - detail: string; - source?: { - pointer?: string; - parameter?: string; +export interface JsonApiData> + extends Omit, 'data'> { + data: Omit, 'attributes'> & { + attributes: Entity; }; } -export interface JsonApiError extends JsonApi { - errors: JsonApiErrorObject[]; +export interface JsonApiError extends Omit { + errors: Omit[]; } export interface JsonApiPagination { diff --git a/backend/node/src/infrastructure/server/express/middlewares/error.ts b/backend/node/src/infrastructure/server/express/middlewares/error.ts index 60c8ac9..ca96e51 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/error.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/error.ts @@ -2,10 +2,7 @@ import type { ErrorObject } from 'ajv'; import type { NextFunction, Request, Response } from 'express'; import { ValidationError } from 'express-json-validator-middleware'; import { ReasonPhrases, StatusCodes } from 'http-status-codes'; -import type { - JsonApiError, - JsonApiErrorObject, -} from '../../../../adapters/interfaces/http.interface'; +import type { JsonApiError } from '../../../../adapters/interfaces/http.interface'; import { NotFoundException } from '../../../../core/exceptions/not-found.exception'; import type { HttpError } from '../../../../core/interfaces/http.interface'; @@ -15,12 +12,12 @@ export function notFoundHandler() { }; } -function createErrorArray(arr: ErrorObject[]): JsonApiErrorObject[] { +function createErrorArray(arr: ErrorObject[]): JsonApiError['errors'] { return arr .filter((item) => item.keyword !== 'if') - .map((item) => { + .map((item) => { return { - status: StatusCodes.BAD_REQUEST, + status: StatusCodes.BAD_REQUEST.toString(), title: ReasonPhrases.BAD_REQUEST, detail: item.message as string, }; @@ -28,7 +25,12 @@ function createErrorArray(arr: ErrorObject[]): JsonApiErrorObject[] { } export function errorHandler() { - return (err: HttpError, req: Request, res: Response, _next: NextFunction) => { + return ( + err: HttpError, + _req: Request, + res: Response, + _next: NextFunction + ) => { const { name, message } = err; let { status } = err; @@ -36,12 +38,9 @@ export function errorHandler() { jsonapi: { version: '1.1', }, - links: { - self: req.path, - }, errors: [ { - status, + status: status?.toString(), title: name, detail: message, }, diff --git a/backend/node/src/infrastructure/server/express/middlewares/logger.ts b/backend/node/src/infrastructure/server/express/middlewares/logger.ts index 60c3600..d054f1f 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/logger.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/logger.ts @@ -20,7 +20,7 @@ export function errorLogger() { _res: Response, next: NextFunction ) => { - log.error('Error log', { + log.error('Error log:', { err, }); From 8be5d33e6c1ee67b245b3437bb2c2c7d71afc200 Mon Sep 17 00:00:00 2001 From: Husen Date: Tue, 19 Mar 2024 22:56:40 +0700 Subject: [PATCH 6/8] build: migrate to devcontainer for development environment Signed-off-by: Husen --- .devcontainer/Dockerfile | 8 + .devcontainer/devcontainer.json | 21 ++ .devcontainer/docker-compose.yml | 210 ++++++++++++++++++ .github/workflows/node.yml | 19 +- .gitignore | 2 + README.md | 2 +- backend/node/.env.testing | 2 +- backend/node/jest.config.js | 1 + backend/node/src/infrastructure/config/app.ts | 2 +- .../src/infrastructure/config/database.ts | 2 +- .../hooks/after-profile-setting.jsonnet | 10 + config/kratos/email-password/kratos.yml | 16 +- docker-compose-express.yml | 52 ----- docker-compose-ory.yml | 103 --------- docker-compose.yml | 101 --------- express.Dockerfile | 10 - shell.nix | 20 -- 17 files changed, 285 insertions(+), 296 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 config/kratos/email-password/hooks/after-profile-setting.jsonnet delete mode 100644 docker-compose-express.yml delete mode 100644 docker-compose-ory.yml delete mode 100644 docker-compose.yml delete mode 100644 express.Dockerfile delete mode 100644 shell.nix diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..a2c985c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 + +ENV HOME=/home/vscode + +# Install dependencies +RUN sudo apt-get update +RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes +RUN echo 'eval "$(starship init bash)"' >> $HOME/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8d23b2a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "name": "Playground", + "dockerComposeFile": "./docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "overrideCommand": true, + "shutdownAction": "stopCompose", + "features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.22" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + } + }, + "customizations": { + "vscode": { + "extensions": ["EditorConfig.EditorConfig", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "svelte.svelte-vscode", "golang.go", "Grafana.vscode-jsonnet", "redhat.vscode-yaml", "ionutvmi.path-autocomplete", "GitHub.vscode-github-actions"] + } + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..75a1668 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,210 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ../:/workspaces:cached + command: sleep infinity + networks: + - playground + + postgres: + image: docker.io/library/postgres:16-alpine + ports: + - '${DB_PORT:-5432}:5432' + volumes: + - 'postgres-data:/var/lib/postgresql/data' + environment: + POSTGRES_USER: ${DB_USERNAME:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_DATABASE:-postgres} + healthcheck: + test: + [ + 'CMD', + 'pg_isready', + '-q', + '-d', + '${DB_USERNAME:-postgres}', + '-U', + '${DB_PASSWORD:-postgres}', + ] + retries: 3 + timeout: 5s + networks: + - playground + + redis: + image: docker.io/library/redis:7-alpine + ports: + - '${REDIS_PORT:-6379}:6379' + volumes: + - 'redis-data:/data' + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + retries: 3 + timeout: 5s + networks: + - playground + + minio: + image: docker.io/minio/minio + ports: + - '${S3_PORT:-9000}:9000' + - '${S3_CONSOLE_PORT:-8900}:8900' + volumes: + - 'minio-data:/data/minio' + environment: + MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:-miniosudo} + MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:-miniosudo} + command: minio server /data/minio --console-address ":8900" + healthcheck: + test: ['CMD', 'curl', '-f', 'http://127.0.0.1:9000/minio/health/live'] + retries: 3 + timeout: 5s + networks: + - playground + + minio-client: + image: docker.io/minio/mc + depends_on: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc config host add myminio http://minio:9000 ${S3_ACCESS_KEY_ID:-miniosudo} ${S3_SECRET_ACCESS_KEY:-miniosudo}; + /usr/bin/mc rm -r --force myminio/local; + /usr/bin/mc mb myminio/local; + /usr/bin/mc policy set download myminio/local; + exit 0; + " + networks: + - playground + + # mailhog: + # image: docker.io/mailhog/mailhog:v1.0.0 + # ports: + # - '${MAILHOG_PORT:-1025}:1025' + # - '${MAILHOG_DASHBOARD_PORT:-8025}:8025' + # volumes: + # - ../config:/.config + # environment: + # - MH_AUTH_FILE="/.config/mailhog/auth" + # networks: + # - playground + + mailslurper: + image: docker.io/oryd/mailslurper:latest-smtps + ports: + - '4436:4436' + - '4437:4437' + networks: + - playground + + postgres-kratos: + image: docker.io/library/postgres:16-alpine + ports: + - '${DB_PORT:-5431}:5432' + volumes: + - 'kratos-postgres:/var/lib/postgresql/data' + environment: + POSTGRES_USER: ${DB_USERNAME:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_DATABASE:-postgres} + healthcheck: + test: + [ + 'CMD', + 'pg_isready', + '-q', + '-d', + '${DB_USERNAME:-postgres}', + '-U', + '${DB_PASSWORD:-postgres}', + ] + retries: 3 + timeout: 5s + networks: + - playground + + kratos-migrate: + image: docker.io/oryd/kratos:v1.0.0 + depends_on: + - postgres-kratos + environment: + - DSN=postgres://postgres:postgres@postgres-kratos:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 + volumes: + - type: bind + source: ../config/kratos/email-password + target: /etc/config/kratos + command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes + restart: on-failure + networks: + - playground + + kratos: + image: docker.io/oryd/kratos:v1.0.0 + depends_on: + - kratos-migrate + ports: + - '4433:4433' # public + - '4434:4434' # admin + restart: unless-stopped + environment: + - DSN=postgres://postgres:postgres@postgres-kratos:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 + - LOG_LEVEL=trace + - SERVE_PUBLIC_BASE_URL=http://127.0.0.1:4455/.ory/kratos/public/ + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + volumes: + - type: bind + source: ../config/kratos/email-password + target: /etc/config/kratos + networks: + - playground + + kratos-selfservice-ui-node: + image: docker.io/oryd/kratos-selfservice-ui-node:v0.13.0-20 + depends_on: + - kratos + restart: on-failure + ports: + - 4435:4435 + environment: + - PORT=4435 + - KRATOS_BROWSER_URL=http://127.0.0.1:4455/.ory/kratos/public + - JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json + - SECURITY_MODE=jwks + - COOKIE_SECRET=playground12345 + - CSRF_COOKIE_NAME=__locahost-example.com-x-csrf-token + - CSRF_COOKIE_SECRET=playground12345 + - KRATOS_PUBLIC_URL=http://kratos:4433/ + networks: + - playground + + oathkeeper: + image: docker.io/oryd/oathkeeper:v0.40.6 + depends_on: + - kratos + ports: + - 4455:4455 + - 4456:4456 + command: + serve proxy -c "/etc/config/oathkeeper/oathkeeper.yml" + environment: + - LOG_LEVEL=debug + restart: on-failure + networks: + - playground + volumes: + - ../config/oathkeeper:/etc/config/oathkeeper + +volumes: + postgres-data: + redis-data: + minio-data: + kratos-postgres: + kratos-sqlite: +networks: + playground: diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 677fdb6..0331490 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -20,6 +20,20 @@ jobs: build: name: Build (Node v${{ matrix.node }}) runs-on: ubuntu-latest + services: + postgres: + image: docker.io/library/postgres:14-alpine + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres strategy: matrix: node: ['20', '21'] @@ -71,11 +85,8 @@ jobs: - name: Run tests run: | cd ${{ env.FOLDER }} - cp .env.example .env - docker compose up -d - sleep 10s + cp .env.testing .env pnpm migrate:up pnpm seed:run pnpm test - docker compose down -v diff --git a/.gitignore b/.gitignore index 8aa360b..2878474 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ # Dependency directories node_modules + +*.pnpm-store diff --git a/README.md b/README.md index 58e7511..eaa090a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # templates -- JWT authentification (with refresh token) \ No newline at end of file +- JWT authentification (with refresh token). \ No newline at end of file diff --git a/backend/node/.env.testing b/backend/node/.env.testing index bc5ec69..4844309 100644 --- a/backend/node/.env.testing +++ b/backend/node/.env.testing @@ -14,7 +14,7 @@ AWS_DEFAULT_REGION=ap-southeast-1 AWS_ACCESS_KEY_ID=miniosudo AWS_SECRET_ACCESS_KEY=miniosudo -REDIS_HOST=redis +REDIS_HOST=localhost REDIS_PORT=6379 JWT_ACCESS_SECRET=jwtaccesssecret diff --git a/backend/node/jest.config.js b/backend/node/jest.config.js index c0ac42f..1d21562 100644 --- a/backend/node/jest.config.js +++ b/backend/node/jest.config.js @@ -4,6 +4,7 @@ const config = { collectCoverage: true, coverageDirectory: 'coverage', coverageProvider: 'v8', + setupFiles: ['dotenv/config'], // globalTeardown: './__tests__/teardown.ts', testMatch: ['**/src/**/*.spec.ts', '**/__tests__/**/*.test.ts'], transform: { diff --git a/backend/node/src/infrastructure/config/app.ts b/backend/node/src/infrastructure/config/app.ts index 1a3c741..da58949 100644 --- a/backend/node/src/infrastructure/config/app.ts +++ b/backend/node/src/infrastructure/config/app.ts @@ -6,7 +6,7 @@ interface AppConfig { } export const appConfig: AppConfig = { - name: process.env.APP_NAME || 'express', + name: process.env.APP_NAME || 'playground', host: process.env.APP_HOST || 'localhost', port: parseInt(String(process.env.APP_PORT), 10) || 3000, url: process.env.APP_URL || 'http://localhost:3000', diff --git a/backend/node/src/infrastructure/config/database.ts b/backend/node/src/infrastructure/config/database.ts index c0ebf3d..3c2a764 100644 --- a/backend/node/src/infrastructure/config/database.ts +++ b/backend/node/src/infrastructure/config/database.ts @@ -3,7 +3,7 @@ import type { Knex } from 'knex'; export const databaseConfig: Knex.Config = { client: 'pg', connection: { - host: process.env.DB_HOST || '127.0.0.1', + host: process.env.DB_HOST || 'postgres', port: parseInt(String(process.env.DB_PORT), 10) || 5432, user: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'postgres', diff --git a/config/kratos/email-password/hooks/after-profile-setting.jsonnet b/config/kratos/email-password/hooks/after-profile-setting.jsonnet new file mode 100644 index 0000000..03f4a4b --- /dev/null +++ b/config/kratos/email-password/hooks/after-profile-setting.jsonnet @@ -0,0 +1,10 @@ +function(ctx) { + data: { + type: "users", + attributes: { + first_name: ctx.identity.traits.name.first, + last_name: ctx.identity.traits.name.last, + email: ctx.identity.traits.email, + } + } +} diff --git a/config/kratos/email-password/kratos.yml b/config/kratos/email-password/kratos.yml index 3ef2630..d413bd5 100644 --- a/config/kratos/email-password/kratos.yml +++ b/config/kratos/email-password/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome allowed_return_urls: - http://127.0.0.1:4455 - http://localhost:19006/Callback @@ -39,6 +39,18 @@ selfservice: ui_url: http://127.0.0.1:4455/settings privileged_session_max_age: 15m required_aal: highest_available + # after: + # profile: + # hooks: + # - hook: web_hook + # config: + # url: | + # http://backend:3000/users/{ identityId } + # method: PATCH + # body: file:///etc/config/kratos/hooks/after-profile-setting.jsonnet + # response: + # ignore: true + # parse: false recovery: enabled: true @@ -50,7 +62,7 @@ selfservice: ui_url: http://127.0.0.1:4455/verification use: code after: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome logout: after: diff --git a/docker-compose-express.yml b/docker-compose-express.yml deleted file mode 100644 index a460809..0000000 --- a/docker-compose-express.yml +++ /dev/null @@ -1,52 +0,0 @@ -version: '3.8' - -services: - backend-migrate: - image: playground-backend-express - depends_on: - - postgres - build: - context: ./ - dockerfile: ./express.Dockerfile - volumes: - - ./backend/node:/app/node - environment: - DB_HOST: postgres - command: pnpm migrate:up - restart: on-failure - networks: - - playground - - backend-seeder: - image: playground-backend-express - depends_on: - - backend-migrate - build: - context: ./ - dockerfile: ./express.Dockerfile - volumes: - - ./backend/node:/app/node - environment: - DB_HOST: postgres - command: pnpm seed:run - restart: on-failure - networks: - - playground - - backend: - image: playground-backend-express - depends_on: - - backend-migrate - - backend-seeder - build: - context: ./ - dockerfile: ./express.Dockerfile - ports: - - 3000:3000 - volumes: - - ./backend/node:/app/node - environment: - DB_HOST: postgres - restart: on-failure - networks: - - playground diff --git a/docker-compose-ory.yml b/docker-compose-ory.yml deleted file mode 100644 index 9e27c46..0000000 --- a/docker-compose-ory.yml +++ /dev/null @@ -1,103 +0,0 @@ -version: '3.8' - -services: - postgres-kratos: - image: docker.io/library/postgres:16-alpine - ports: - - '${DB_PORT:-5431}:5432' - volumes: - - 'kratos-postgres:/var/lib/postgresql/data' - environment: - POSTGRES_USER: ${DB_USERNAME:-postgres} - POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} - POSTGRES_DB: ${DB_DATABASE:-postgres} - healthcheck: - test: - [ - 'CMD', - 'pg_isready', - '-q', - '-d', - '${DB_USERNAME:-postgres}', - '-U', - '${DB_PASSWORD:-postgres}', - ] - retries: 3 - timeout: 5s - networks: - - playground - - kratos-migrate: - image: docker.io/oryd/kratos:v1.0.0 - depends_on: - - postgres-kratos - environment: - - DSN=postgres://postgres:postgres@postgres-kratos:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 - volumes: - - type: bind - source: ./config/kratos/email-password - target: /etc/config/kratos - command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes - restart: on-failure - networks: - - playground - - kratos: - image: docker.io/oryd/kratos:v1.0.0 - depends_on: - - kratos-migrate - ports: - - '4433:4433' # public - - '4434:4434' # admin - restart: unless-stopped - environment: - - DSN=postgres://postgres:postgres@postgres-kratos:5432/postgres?sslmode=disable&max_conns=20&max_idle_conns=4 - - LOG_LEVEL=trace - - SERVE_PUBLIC_BASE_URL=http://127.0.0.1:4455/.ory/kratos/public/ - command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier - volumes: - - type: bind - source: ./config/kratos/email-password - target: /etc/config/kratos - networks: - - playground - - kratos-selfservice-ui-node: - image: docker.io/oryd/kratos-selfservice-ui-node:v0.13.0-20 - depends_on: - - kratos - restart: on-failure - ports: - - 4435:4435 - environment: - - PORT=4435 - - KRATOS_BROWSER_URL=http://127.0.0.1:4455/.ory/kratos/public - - JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json - - SECURITY_MODE=jwks - - COOKIE_SECRET=playground12345 - - CSRF_COOKIE_NAME=__locahost-example.com-x-csrf-token - - CSRF_COOKIE_SECRET=playground12345 - - KRATOS_PUBLIC_URL=http://kratos:4433/ - networks: - - playground - - oathkeeper: - image: docker.io/oryd/oathkeeper:v0.40.6 - depends_on: - - kratos - ports: - - 4455:4455 - - 4456:4456 - command: - serve proxy -c "/etc/config/oathkeeper/oathkeeper.yml" - environment: - - LOG_LEVEL=debug - restart: on-failure - networks: - - playground - volumes: - - ./config/oathkeeper:/etc/config/oathkeeper - -volumes: - kratos-postgres: - kratos-sqlite: diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 9527bdb..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,101 +0,0 @@ -version: '3.8' - -services: - postgres: - image: docker.io/library/postgres:16-alpine - ports: - - '${DB_PORT:-5432}:5432' - volumes: - - 'postgres-data:/var/lib/postgresql/data' - environment: - POSTGRES_USER: ${DB_USERNAME:-postgres} - POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} - POSTGRES_DB: ${DB_DATABASE:-postgres} - healthcheck: - test: - [ - 'CMD', - 'pg_isready', - '-q', - '-d', - '${DB_USERNAME:-postgres}', - '-U', - '${DB_PASSWORD:-postgres}', - ] - retries: 3 - timeout: 5s - networks: - - playground - - redis: - image: docker.io/library/redis:7-alpine - ports: - - '${REDIS_PORT:-6379}:6379' - volumes: - - 'redis-data:/data' - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - retries: 3 - timeout: 5s - networks: - - playground - - minio: - image: docker.io/minio/minio - ports: - - '${S3_PORT:-9000}:9000' - - '${S3_CONSOLE_PORT:-8900}:8900' - volumes: - - 'minio-data:/data/minio' - environment: - MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:-miniosudo} - MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:-miniosudo} - command: minio server /data/minio --console-address ":8900" - healthcheck: - test: ['CMD', 'curl', '-f', 'http://127.0.0.1:9000/minio/health/live'] - retries: 3 - timeout: 5s - networks: - - playground - - minio-client: - image: docker.io/minio/mc - depends_on: - - minio - entrypoint: > - /bin/sh -c " - /usr/bin/mc config host add myminio http://minio:9000 ${S3_ACCESS_KEY_ID:-miniosudo} ${S3_SECRET_ACCESS_KEY:-miniosudo}; - /usr/bin/mc rm -r --force myminio/local; - /usr/bin/mc mb myminio/local; - /usr/bin/mc policy set download myminio/local; - exit 0; - " - networks: - - playground - - # mailhog: - # image: docker.io/mailhog/mailhog:v1.0.0 - # ports: - # - '${MAILHOG_PORT:-1025}:1025' - # - '${MAILHOG_DASHBOARD_PORT:-8025}:8025' - # volumes: - # - ../config:/.config - # environment: - # - MH_AUTH_FILE="/.config/mailhog/auth" - # networks: - # - playground - - mailslurper: - image: docker.io/oryd/mailslurper:latest-smtps - ports: - - '4436:4436' - - '4437:4437' - networks: - - playground - -volumes: - postgres-data: - redis-data: - minio-data: -networks: - playground: diff --git a/express.Dockerfile b/express.Dockerfile deleted file mode 100644 index 60b8f3f..0000000 --- a/express.Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM docker.io/library/node:20-bookworm -ADD ./openapi.yaml ./openapi.yaml -ADD ./backend/node /app/node -WORKDIR /app/node -RUN rm -rf node_modules -RUN rm -rf dist -RUN npm install -g pnpm - -EXPOSE 3000 -CMD ["pnpm", "dev"] diff --git a/shell.nix b/shell.nix deleted file mode 100644 index cedfe62..0000000 --- a/shell.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ pkgs ? import {} }: -pkgs.mkShell { - buildInputs = [ - pkgs.jsonnet-language-server - pkgs.yaml-language-server - pkgs.nil - pkgs.nixpkgs-fmt - pkgs.vscode-langservers-extracted - pkgs.dockerfile-language-server-nodejs - pkgs.marksman - pkgs.go - pkgs.gopls - pkgs.gotools - pkgs.delve - pkgs.nodejs_20 - pkgs.nodePackages.pnpm - pkgs.nodePackages.typescript-language-server - pkgs.nodePackages.bash-language-server - ]; -} From 0d0807ed363114beba30d8a5cfd4925fbaa87746 Mon Sep 17 00:00:00 2001 From: Husen Date: Mon, 13 May 2024 07:11:12 +0700 Subject: [PATCH 7/8] feat: update user using put method Signed-off-by: Husen --- .devcontainer/devcontainer.json | 2 +- .devcontainer/docker-compose.yml | 2 +- backend/node/.env.example | 10 +- backend/node/__tests__/user.test.ts | 534 ++++++++++++++++++ .../src/core/entities/validation.entity.ts | 2 + .../node/src/infrastructure/config/auth.ts | 31 - .../server/express/handlers/user.handler.ts | 63 ++- .../express/middlewares/unique-user-email.ts | 8 +- .../server/express/routes/user.route.ts | 9 + .../server/express/schemas/user.schema.ts | 21 + openapi.yaml | 47 +- 11 files changed, 679 insertions(+), 50 deletions(-) delete mode 100644 backend/node/src/infrastructure/config/auth.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8d23b2a..992ca0a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ "name": "Playground", "dockerComposeFile": "./docker-compose.yml", "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "workspaceFolder": "/workspaces", "overrideCommand": true, "shutdownAction": "stopCompose", "features": { diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 75a1668..3788cb9 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -38,7 +38,7 @@ services: - playground redis: - image: docker.io/library/redis:7-alpine + image: docker.io/library/redis:7.2.4-alpine ports: - '${REDIS_PORT:-6379}:6379' volumes: diff --git a/backend/node/.env.example b/backend/node/.env.example index e17c7d8..c64ec12 100644 --- a/backend/node/.env.example +++ b/backend/node/.env.example @@ -1,7 +1,8 @@ APP_NAME=Playground APP_PORT=3000 +APP_URL=http://localhost:3000 -DB_HOST=127.0.0.1 +DB_HOST=postgres DB_PORT=5432 DB_USERNAME=postgres DB_PASSWORD=postgres @@ -13,10 +14,5 @@ AWS_DEFAULT_REGION=ap-southeast-1 AWS_ACCESS_KEY_ID=miniosudo AWS_SECRET_ACCESS_KEY=miniosudo -REDIS_HOST=127.0.0.1 +REDIS_HOST=redis REDIS_PORT=6379 - -JWT_ACCESS_SECRET=jwtaccesssecret -JWT_REFRESH_SECRET=jwtrefreshsecret -JWT_ISSUER=jwtaccessissuer -JWT_AUDIENCE=jwtaccessaudience diff --git a/backend/node/__tests__/user.test.ts b/backend/node/__tests__/user.test.ts index eb4ef73..b6d457e 100644 --- a/backend/node/__tests__/user.test.ts +++ b/backend/node/__tests__/user.test.ts @@ -880,3 +880,537 @@ describe('PATCH /users/{id}', () => { ); }); }); + +describe('PUT /users', () => { + test('should return error when request body is empty', async () => { + const response: SupertestResponse = await request + .put('/users') + .set('Accept', 'application/vnd.api+json'); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.required'), + }, + ], + }); + }); + + test('should return error when data is empty object', async () => { + const response: SupertestResponse = await request + .put('/users') + .set('Accept', 'application/vnd.api+json') + .send({ + data: {}, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.type.required'), + }, + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.id.required'), + }, + ], + }); + }); + + test('should return error when data.type is invalid', async () => { + const response: SupertestResponse = await request + .put('/users') + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + type: 'user', + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.type.pattern'), + }, + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.id.required'), + }, + ], + }); + }); + + test('should return error when data.attributes.id is missing', async () => { + const response: SupertestResponse = await request + .put('/users') + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + type: 'users', + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.id.required'), + }, + ], + }); + }); + + test('should not update user', async () => { + const response: SupertestResponse> = await request + .put('/users') + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + }, + }); + + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id', + user.id + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.first_name', + user.first_name + ); + expect>(response.body).toHaveProperty( + 'data.attributes.last_name', + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.email', + user.email + ); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); + }); + + test('should return error when name is null', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + type: 'users', + id: user.id, + attributes: { + first_name: null, + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.first_name.type'), + }, + ], + }); + }); + + test('should return error when name is null', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + first_name: null, + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.first_name.type'), + }, + ], + }); + }); + + test('should return error when name is empty', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + first_name: '', + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.first_name.minLength'), + }, + ], + }); + }); + + test('should return error when name is invalid', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + first_name: 12345, + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.first_name.type'), + }, + ], + }); + }); + + test('should ok when username is null', async () => { + const response: SupertestResponse> = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + nickname: null, + }, + }, + }); + + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id', + user.id + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.first_name', + user.first_name + ); + expect>(response.body).toHaveProperty( + 'data.attributes.last_name', + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.email', + user.email + ); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); + }); + + test('should ok when username is empty', async () => { + const response: SupertestResponse> = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + nickname: '', + }, + }, + }); + + expect(response.status).toStrictEqual(StatusCodes.OK); + expect>(response.body).toHaveProperty( + 'jsonapi.version', + '1.1' + ); + expect>(response.body).toHaveProperty( + 'data.id', + user.id + ); + expect>(response.body).toHaveProperty( + 'data.type', + 'users' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.first_name', + user.first_name + ); + expect>(response.body).toHaveProperty( + 'data.attributes.last_name', + String(user.last_name) + ); + expect>(response.body).toHaveProperty( + 'data.attributes.nickname', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.email', + user.email + ); + expect>(response.body).toHaveProperty( + 'data.attributes.photo', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.avatar', + null + ); + expect>(response.body).toHaveProperty( + 'data.attributes.created_at' + ); + expect>(response.body).toHaveProperty( + 'data.attributes.updated_at' + ); + }); + + test('should return error when email is null', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + email: null, + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.type'), + }, + ], + }); + }); + + test('should return error when email is empty', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + email: '', + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.format'), + }, + ], + }); + }); + + test('should return error when email is invalid', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + email: 12345, + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.type'), + }, + ], + }); + }); + + test('should return error when email is invalid', async () => { + const response: SupertestResponse = await request + .put(`/users`) + .set('Accept', 'application/vnd.api+json') + .send({ + data: { + id: user.id, + type: 'users', + attributes: { + email: 'johndoe', + }, + }, + }); + + expect(response.status).toStrictEqual( + StatusCodes.BAD_REQUEST + ); + expect(response.body).toStrictEqual({ + jsonapi: { + version: '1.1', + }, + errors: [ + { + status: StatusCodes.BAD_REQUEST.toString(), + title: ReasonPhrases.BAD_REQUEST, + detail: getErrorMessage('data.attributes.email.format'), + }, + ], + }); + }); +}); diff --git a/backend/node/src/core/entities/validation.entity.ts b/backend/node/src/core/entities/validation.entity.ts index 5be9789..dd51c3c 100644 --- a/backend/node/src/core/entities/validation.entity.ts +++ b/backend/node/src/core/entities/validation.entity.ts @@ -24,6 +24,7 @@ interface ErrorSchemaError { 'id.required': string; 'id.format': string; 'user.exist': string; + 'data.id.required': string; } const errorMessage: ErrorSchemaError = { @@ -58,6 +59,7 @@ const errorMessage: ErrorSchemaError = { 'id.required': 'Validation failed (uuid v4 is expected).', 'id.format': 'Validation failed (uuid v4 is expected).', 'user.exist': 'The user is not found.', + 'data.id.required': 'The data.attributes.id property is required.', }; export function getErrorMessage(key: keyof ErrorSchemaError): string { diff --git a/backend/node/src/infrastructure/config/auth.ts b/backend/node/src/infrastructure/config/auth.ts deleted file mode 100644 index 562f9af..0000000 --- a/backend/node/src/infrastructure/config/auth.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { parse } from '@lukeed/ms'; -import type { SignerOptions } from 'fast-jwt'; - -type JwtConfig = Pick< - SignerOptions, - 'algorithm' | 'expiresIn' | 'iss' | 'aud' | 'mutatePayload' -> & { key: string }; - -interface AuthConfig { - access: JwtConfig; - refresh: JwtConfig; -} - -export const authConfig: AuthConfig = { - access: { - key: process.env.JWT_ACCESS_SECRET || 'jwtaccesssecret', - algorithm: 'HS512', - expiresIn: parse('15m') as number, - iss: process.env.JWT_ISSUER || 'jwtaccessissuer', - aud: process.env.JWT_ISSUER || 'jwtaccessissuer', - mutatePayload: true, - }, - refresh: { - key: process.env.JWT_REFRESH_SECRET || 'jwtrefreshsecret', - algorithm: 'HS512', - expiresIn: parse('30d') as number, - iss: process.env.JWT_ISSUER || 'jwtaccessissuer', - aud: process.env.JWT_ISSUER || 'jwtaccessissuer', - mutatePayload: true, - }, -}; diff --git a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts index 34a3e67..ce71631 100644 --- a/backend/node/src/infrastructure/server/express/handlers/user.handler.ts +++ b/backend/node/src/infrastructure/server/express/handlers/user.handler.ts @@ -36,7 +36,7 @@ export async function create( req: Request>, res: Response>> ) { - let request: HttpRequestBody = { + const request: HttpRequestBody = { body: { ...req.body.data.attributes, id: req.body.data.id, @@ -46,8 +46,10 @@ export async function create( if (req.file) { const { filename, size, mimetype, path, originalname } = req.file; - request = { - ...request, + Object.assign< + HttpRequestBody, + Partial> + >(request, { file: { name: filename, size, @@ -55,7 +57,7 @@ export async function create( extension: extname(originalname), content: createReadStream(path), }, - }; + }); } const controller = createUserController(userRepository); @@ -132,7 +134,7 @@ export async function update( req: Request>, res: Response>> ) { - let request: HttpRequest = { + const request: HttpRequest = { params: req.params, body: { ...req.body.data.attributes, @@ -143,8 +145,10 @@ export async function update( if (req.file) { const { filename, size, mimetype, path, originalname } = req.file; - request = { - ...request, + Object.assign< + HttpRequest, + Partial> + >(request, { file: { name: filename, size, @@ -152,7 +156,7 @@ export async function update( extension: extname(originalname), content: createReadStream(path), }, - }; + }); } const controller = updateUserController(userRepository, fileService); @@ -168,6 +172,49 @@ export async function update( res.status(status).contentType('application/vnd.api+json').json(result); } +export async function compatUpdate( + req: Request>, + res: Response>> +) { + const request: HttpRequest = { + body: { + ...req.body.data.attributes, + id: req.body.data.id, + }, + params: { + id: req.body.data.id, + }, + }; + + if (req.file) { + const { filename, size, mimetype, path, originalname } = req.file; + + Object.assign< + HttpRequest, + Partial> + >(request, { + file: { + name: filename, + size, + type: mimetype, + extension: extname(originalname), + content: createReadStream(path), + }, + }); + } + + const controller = updateUserController(userRepository, fileService); + const { status, data } = await controller(request); + + const result = await userSerializer.serialize(data, { + linkers: { + resource: UserLinker, + }, + }); + + res.status(status).contentType('application/vnd.api+json').json(result); +} + export async function remove(req: Request, res: Response) { const controller = removeUserController(userRepository, fileService); diff --git a/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts b/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts index d2cf122..4d35b1a 100644 --- a/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts +++ b/backend/node/src/infrastructure/server/express/middlewares/unique-user-email.ts @@ -17,11 +17,17 @@ export function uniqueUserEmail() { req: Request< Pick, unknown, - JsonApiData> + JsonApiData> >, _res: Response, next: NextFunction ) => { + if (!req.params?.id && req.body.data?.id) { + Object.assign(req.params, { + id: req.body.data.id, + }); + } + const httpRequest: HttpRequest< unknown, Pick, diff --git a/backend/node/src/infrastructure/server/express/routes/user.route.ts b/backend/node/src/infrastructure/server/express/routes/user.route.ts index b86d4cd..31b0e86 100644 --- a/backend/node/src/infrastructure/server/express/routes/user.route.ts +++ b/backend/node/src/infrastructure/server/express/routes/user.route.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; import { + compatUpdate, create, findAll, findOne, @@ -11,6 +12,7 @@ import { uniqueUserEmail } from '../middlewares/unique-user-email'; import { validate } from '../middlewares/validator'; import { idParamSchema } from '../schemas/common.schema'; import { + compatUpdateUserSchema, createUserSchema, findAllUserSchema, updateUserSchema, @@ -44,6 +46,13 @@ userRouter.patch( asyncHandler(update) ); +userRouter.put( + '/', + [validate(compatUpdateUserSchema)], + uniqueUserEmail(), + asyncHandler(compatUpdate) +); + userRouter.delete( '/:id', validate(idParamSchema, 'params'), diff --git a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts index 23c22db..dee4071 100644 --- a/backend/node/src/infrastructure/server/express/schemas/user.schema.ts +++ b/backend/node/src/infrastructure/server/express/schemas/user.schema.ts @@ -243,3 +243,24 @@ export const updateUserSchema: AllowedSchema = { dependencies: {}, }, }; + +const copiedUpdateUserSchema = structuredClone(updateUserSchema); +export const compatUpdateUserSchema: AllowedSchema = { + ...copiedUpdateUserSchema, + properties: { + ...copiedUpdateUserSchema.properties, + data: { + ...copiedUpdateUserSchema.properties?.data, + required: [ + ...(copiedUpdateUserSchema.properties?.data?.required as string[]), + 'id', + ], + errorMessage: { + required: { + type: getErrorMessage('data.type.required'), + id: getErrorMessage('data.id.required'), + }, + }, + }, + }, +}; diff --git a/openapi.yaml b/openapi.yaml index 79c381a..8c17576 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -62,6 +62,51 @@ paths: - example: links: self: "/user" + put: + tags: + - "user" + summary: Update an user (for ORY compatibility). + operationId: update-compat + requestBody: + content: + "application/vnd.api+json": + schema: + allOf: + - $ref: "#/components/schemas/UserRequestData" + - example: + links: + self: "/user" + responses: + "200": + description: Return updated user. + content: + "application/vnd.api+json": + schema: + allOf: + - $ref: "#/components/schemas/SingleUserResponseData" + - example: + links: + self: "/user" + "400": + description: Invalid request body + content: + "application/vnd.api+json": + schema: + allOf: + - $ref: "#/components/schemas/UserRequestError" + - example: + links: + self: "/user" + "500": + description: Internal server error. + content: + "application/vnd.api+json": + schema: + allOf: + - $ref: "#/components/schemas/InternalServerError" + - example: + links: + self: "/user" get: tags: - "user" @@ -261,7 +306,7 @@ components: type: string example: jsonapi: - version: "1.0" + version: "1.1" Data: type: object properties: From 652f28ff6ac7e2c9e74c7954469b7221459381b9 Mon Sep 17 00:00:00 2001 From: Husen Date: Sat, 7 Sep 2024 17:33:45 +0700 Subject: [PATCH 8/8] ci: add node version 22 Signed-off-by: Husen --- .github/workflows/node.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 0331490..89770fd 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: docker.io/library/postgres:14-alpine + image: docker.io/library/postgres:16-alpine options: >- --health-cmd pg_isready --health-interval 10s @@ -36,7 +36,7 @@ jobs: POSTGRES_DB: postgres strategy: matrix: - node: ['20', '21'] + node: ['20', '21', '22'] env: FOLDER: backend/node @@ -47,7 +47,7 @@ jobs: - name: Set up pnpm uses: pnpm/action-setup@v2 with: - version: 8 + version: 9 run_install: false - name: Set up Node