Compare commits

3 Commits

Author SHA1 Message Date
90e5624576 chore: set up commitlint and git hooks
Some checks failed
Commit Message Check / conventional-commits (pull_request) Failing after 1m53s
Deploy monie-backend to dev (kaniko) / build-and-deploy (pull_request) Successful in 0s
Deploy monie-backend (kaniko) / build-and-deploy (pull_request) Successful in 0s
2026-04-04 23:00:37 +03:00
508bbdf5cc chore(backend): configure commitlint and husky
Some checks failed
Deploy monie-backend to dev (kaniko) / build-and-deploy (push) Has been cancelled
2026-04-03 18:56:00 +03:00
6032451b17 chore: migrate backend to monorepo apps and biome 2026-04-03 18:05:10 +03:00
62 changed files with 2066 additions and 1180 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.git
.gitea
node_modules
dist
coverage
.npm
*.log
.env
.env.*
README.md
.codex

View File

@@ -0,0 +1,50 @@
name: Commit Message Check
on:
push:
branches: [ main, develop ]
pull_request:
jobs:
conventional-commits:
runs-on: [self-hosted, linux]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Validate commit messages with commitlint
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.sha }}
run: |
set -euo pipefail
BASE_SHA=""
if [ "${EVENT_NAME}" = "pull_request" ] && [ -n "${PR_BASE_SHA:-}" ]; then
BASE_SHA="${PR_BASE_SHA}"
elif [ -n "${BEFORE_SHA:-}" ] && [ "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]; then
BASE_SHA="${BEFORE_SHA}"
elif git rev-parse "${HEAD_SHA}^" >/dev/null 2>&1; then
BASE_SHA="$(git rev-parse "${HEAD_SHA}^")"
fi
if [ -n "${BASE_SHA}" ] && [ "${BASE_SHA}" != "${HEAD_SHA}" ]; then
npx --no -- commitlint --from "${BASE_SHA}" --to "${HEAD_SHA}" --verbose
else
git log -1 --format=%s "${HEAD_SHA}" | npx --no -- commitlint --verbose
fi

View File

@@ -0,0 +1,193 @@
name: Deploy monie-backend to dev (kaniko)
on:
push:
branches: [ develop ]
pull_request:
jobs:
build-and-deploy:
runs-on: [self-hosted, linux, k8s]
env:
CI_NS: ci
APP_NS: dev
# Kaniko job runs inside cluster pods and can reach registry via node IP.
PUSH_REGISTRY: 192.168.1.250:32000
# Runtime pull should use the endpoint configured in MicroK8s containerd.
DEPLOY_REGISTRY: localhost:32000
IMAGE: monie-backend
DEPLOYMENT: monie-backend
CONTAINER: monie-backend
# repo без кредов (креды берём из secret внутри Kaniko Job)
REPO_HOST: git.denjs.ru
REPO_PATH: monie/monie-backend.git
steps:
- name: Skip deploy for pull requests
if: github.event_name == 'pull_request'
run: echo "Pull request check passed. Deploy runs only on push to develop."
- name: Debug
if: github.event_name == 'push'
run: |
set -eu
echo "sha=${{ github.sha }}"
echo "ref=${{ github.ref_name }}"
echo "repo=git://${REPO_HOST}/${REPO_PATH}"
microk8s kubectl version --client=true
- name: Build & push with Kaniko (K8s Job)
if: github.event_name == 'push'
env:
SHA: ${{ github.sha }}
REF: ${{ github.ref_name }}
run: |
set -euo pipefail
JOB="kaniko-${SHA}"
DEST="${PUSH_REGISTRY}/${IMAGE}:${SHA}"
echo "JOB=${JOB}"
echo "DEST=${DEST}"
echo "REF=${REF}"
echo "REPO=git://${REPO_HOST}/${REPO_PATH}"
microk8s kubectl -n "${CI_NS}" delete job "${JOB}" --ignore-not-found=true
cat <<EOF | microk8s kubectl -n "${CI_NS}" apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB}
labels:
app: kaniko
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
env:
- name: GIT_USERNAME
value: denis
- name: GIT_PASSWORD
valueFrom:
secretKeyRef:
name: gitea-git-token
key: token
args:
- --context=git://${REPO_HOST}/${REPO_PATH}#refs/heads/${REF}
- --dockerfile=Dockerfile
- --destination=${DEST}
- --verbosity=debug
- --cache=true
- --cache-repo=${PUSH_REGISTRY}/${IMAGE}-cache
- --insecure-registry=${PUSH_REGISTRY}
- --skip-tls-verify-registry=${PUSH_REGISTRY}
ttlSecondsAfterFinished: 3600
EOF
DEADLINE_SECONDS=1800
START_TS="$(date +%s)"
OK=1
while true; do
SUCCEEDED="$(microk8s kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.succeeded}' 2>/dev/null || true)"
FAILED="$(microk8s kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.failed}' 2>/dev/null || true)"
SUCCEEDED="${SUCCEEDED:-0}"
FAILED="${FAILED:-0}"
if [ "${SUCCEEDED}" -ge 1 ]; then
OK=0
break
fi
if [ "${FAILED}" -ge 1 ]; then
OK=1
break
fi
NOW_TS="$(date +%s)"
if [ $((NOW_TS - START_TS)) -ge "${DEADLINE_SECONDS}" ]; then
OK=2
break
fi
sleep 5
done
echo "[ci] job status:"
microk8s kubectl -n "${CI_NS}" get job "${JOB}" -o wide || true
echo "[ci] job logs (tail):"
microk8s kubectl -n "${CI_NS}" logs "job/${JOB}" --tail=300 || true
if [ "${OK}" -ne 0 ]; then
echo "[ci] job did not reach Complete; describing job/pods for debug"
microk8s kubectl -n "${CI_NS}" describe job "${JOB}" || true
microk8s kubectl -n "${CI_NS}" get pods -l job-name="${JOB}" -o wide || true
microk8s kubectl -n "${CI_NS}" describe pod -l job-name="${JOB}" || true
exit 1
fi
- name: Deploy to dev
if: github.event_name == 'push'
env:
SHA: ${{ github.sha }}
run: |
set -euo pipefail
TARGET_IMAGE="${DEPLOY_REGISTRY}/${IMAGE}:${SHA}"
microk8s kubectl -n "${APP_NS}" set image "deployment/${DEPLOYMENT}" \
"${CONTAINER}=${TARGET_IMAGE}"
set +e
microk8s kubectl -n "${APP_NS}" rollout status "deployment/${DEPLOYMENT}" --timeout=15m
ROLLOUT_RC=$?
set -e
if [ "${ROLLOUT_RC}" -ne 0 ]; then
echo "[deploy] rollout did not complete in time; collecting diagnostics"
SELECTOR="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{range $k,$v := .spec.selector.matchLabels}{$k}={$v},{end}' 2>/dev/null || true)"
SELECTOR="${SELECTOR%,}"
microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" -o wide || true
microk8s kubectl -n "${APP_NS}" describe deployment "${DEPLOYMENT}" || true
if [ -n "${SELECTOR}" ]; then
microk8s kubectl -n "${APP_NS}" get rs -l "${SELECTOR}" -o wide || true
microk8s kubectl -n "${APP_NS}" get pods -l "${SELECTOR}" -o wide || true
microk8s kubectl -n "${APP_NS}" describe pods -l "${SELECTOR}" || true
fi
microk8s kubectl -n "${APP_NS}" get events --sort-by=.lastTimestamp | tail -n 100 || true
DESIRED="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.spec.replicas}' 2>/dev/null || true)"
UPDATED="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.updatedReplicas}' 2>/dev/null || true)"
AVAILABLE="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.availableReplicas}' 2>/dev/null || true)"
DESIRED="${DESIRED:-0}"
UPDATED="${UPDATED:-0}"
AVAILABLE="${AVAILABLE:-0}"
echo "[deploy] desired=${DESIRED} updated=${UPDATED} available=${AVAILABLE}"
if [ "${UPDATED}" -ge "${DESIRED}" ] && [ "${AVAILABLE}" -ge "${DESIRED}" ]; then
echo "[deploy] New replica is healthy; old replica termination is delayed. Continuing."
exit 0
fi
exit "${ROLLOUT_RC}"
fi

View File

@@ -0,0 +1,180 @@
name: Deploy monie-backend (kaniko)
on:
push:
branches: [ main ]
pull_request:
jobs:
build-and-deploy:
runs-on: [self-hosted, linux, k8s]
env:
CI_NS: ci
APP_NS: prod
# Kaniko job runs inside cluster pods and can reach registry via node IP.
PUSH_REGISTRY: 192.168.1.250:32000
# Runtime pull should use the endpoint configured in MicroK8s containerd.
DEPLOY_REGISTRY: localhost:32000
IMAGE: monie-backend
DEPLOYMENT: monie-backend
CONTAINER: monie-backend
# repo без кредов (креды берём из secret внутри Kaniko Job)
REPO_HOST: git.denjs.ru
REPO_PATH: monie/monie-backend.git
steps:
- name: Skip deploy for pull requests
if: github.event_name == 'pull_request'
run: echo "Pull request check passed. Deploy runs only on push to main."
- name: Build & push image with Kaniko (K8s Job)
if: github.event_name == 'push'
env:
SHA: ${{ github.sha }}
REF: ${{ github.ref_name }}
run: |
set -euo pipefail
JOB="kaniko-${SHA}"
DEST="${PUSH_REGISTRY}/${IMAGE}:${SHA}"
kubectl -n "${CI_NS}" delete job "${JOB}" --ignore-not-found=true
cat <<EOF | kubectl -n "${CI_NS}" apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB}
spec:
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
imagePullPolicy: IfNotPresent
env:
- name: GIT_USERNAME
value: denis
- name: GIT_PASSWORD
valueFrom:
secretKeyRef:
name: gitea-git-token
key: token
args:
- --dockerfile=Dockerfile
- --context=git://${REPO_HOST}/${REPO_PATH}#refs/heads/${REF}
- --destination=${DEST}
- --verbosity=debug
- --cache=true
- --cache-repo=${PUSH_REGISTRY}/${IMAGE}-cache
- --insecure-registry=${PUSH_REGISTRY}
- --skip-tls-verify-registry=${PUSH_REGISTRY}
EOF
# ждём terminal state job и не висим 30 минут при явном Failed
DEADLINE_SECONDS=1800
START_TS="$(date +%s)"
OK=1
while true; do
SUCCEEDED="$(kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.succeeded}' 2>/dev/null || true)"
FAILED="$(kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.failed}' 2>/dev/null || true)"
SUCCEEDED="${SUCCEEDED:-0}"
FAILED="${FAILED:-0}"
if [ "${SUCCEEDED}" -ge 1 ]; then
OK=0
break
fi
if [ "${FAILED}" -ge 1 ]; then
OK=1
break
fi
NOW_TS="$(date +%s)"
if [ $((NOW_TS - START_TS)) -ge "${DEADLINE_SECONDS}" ]; then
OK=2
break
fi
sleep 5
done
echo "[ci] job status:"
kubectl -n "${CI_NS}" get job "${JOB}" -o wide || true
echo "[ci] job logs (tail):"
kubectl -n "${CI_NS}" logs "job/${JOB}" --tail=300 || true
if [ "${OK}" -ne 0 ]; then
echo "[ci] job did not reach Complete; describing job/pods for debug"
kubectl -n "${CI_NS}" describe job "${JOB}" || true
kubectl -n "${CI_NS}" get pods -l job-name="${JOB}" -o wide || true
kubectl -n "${CI_NS}" describe pod -l job-name="${JOB}" || true
exit 1
fi
- name: Deploy to prod
if: github.event_name == 'push'
env:
SHA: ${{ github.sha }}
run: |
set -euo pipefail
TARGET_IMAGE="${DEPLOY_REGISTRY}/${IMAGE}:${SHA}"
kubectl -n "${APP_NS}" set image "deployment/${DEPLOYMENT}" \
"${CONTAINER}=${TARGET_IMAGE}"
set +e
kubectl -n "${APP_NS}" rollout status "deployment/${DEPLOYMENT}" --timeout=15m
ROLLOUT_RC=$?
set -e
if [ "${ROLLOUT_RC}" -ne 0 ]; then
echo "[deploy] rollout did not complete in time; collecting diagnostics"
SELECTOR="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{range $k,$v := .spec.selector.matchLabels}{$k}={$v},{end}' 2>/dev/null || true)"
SELECTOR="${SELECTOR%,}"
kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" -o wide || true
kubectl -n "${APP_NS}" describe deployment "${DEPLOYMENT}" || true
if [ -n "${SELECTOR}" ]; then
kubectl -n "${APP_NS}" get rs -l "${SELECTOR}" -o wide || true
kubectl -n "${APP_NS}" get pods -l "${SELECTOR}" -o wide || true
kubectl -n "${APP_NS}" describe pods -l "${SELECTOR}" || true
fi
kubectl -n "${APP_NS}" get events --sort-by=.lastTimestamp | tail -n 100 || true
DESIRED="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.spec.replicas}' 2>/dev/null || true)"
UPDATED="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.updatedReplicas}' 2>/dev/null || true)"
AVAILABLE="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.availableReplicas}' 2>/dev/null || true)"
DESIRED="${DESIRED:-0}"
UPDATED="${UPDATED:-0}"
AVAILABLE="${AVAILABLE:-0}"
echo "[deploy] desired=${DESIRED} updated=${UPDATED} available=${AVAILABLE}"
if [ "${UPDATED}" -ge "${DESIRED}" ] && [ "${AVAILABLE}" -ge "${DESIRED}" ]; then
echo "[deploy] New replica is healthy; old replica termination is delayed. Continuing."
exit 0
fi
exit "${ROLLOUT_RC}"
fi

1
.gitignore vendored
View File

@@ -34,6 +34,7 @@ lerna-debug.log*
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
.codex
# dotenv environment variable files # dotenv environment variable files
.env .env

1
.husky/commit-msg Executable file
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit "$1"

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npm test

19
.husky/pre-push Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env sh
branch="$(git rev-parse --abbrev-ref HEAD)"
case "$branch" in
main|develop)
echo "Direct pushes to $branch are not allowed."
echo "Please create a feature/... or bugfix/... branch and open a PR/MR."
exit 1
;;
feature/*|bugfix/*|hotfix/*|chore/*)
exit 0
;;
*)
echo "Invalid branch name: $branch"
echo "Allowed branch prefixes: feature/*, bugfix/*, hotfix/*, chore/*"
exit 1
;;
esac

View File

@@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

142
AGENTS.md Normal file
View File

@@ -0,0 +1,142 @@
# Monie Backend Agent Guide
## Scope
This file applies only to `monie-backend`.
`monie-backend` is the source of truth for business logic across Monie apps:
- `monie-web`
- `monie-mobile`
- `monie-widget`
- `monie-landing` (limited, mostly public marketing integrations)
Do not move backend rules to frontend projects "for convenience".
---
## Current Architecture
NestJS monorepo with multiple apps in `apps/`:
- `gateway` (`apps/gateway`) — public API entrypoint / aggregation layer
- `auth` (`apps/auth`) — authentication and authorization
- `bookings` (`apps/bookings`) — booking-related APIs
- `tasks` (`apps/tasks`) — tasks domain APIs
- `notifications` (`apps/notifications`) — notification domain APIs
Default ports:
- `AUTH_PORT=3000`
- `GATEWAY_PORT=3001`
- `TASKS_PORT=3002`
- `NOTIFICATIONS_PORT=3003`
- `BOOKING_PORT=3004`
---
## Runtime Commands
Use npm scripts from `package.json`:
- `npm run start:dev` — run all apps in watch mode
- `npm run start:gateway`
- `npm run start:auth`
- `npm run start:bookings`
- `npm run start:tasks`
- `npm run start:notifications`
- `npm run build`
- `npm run test`
- `npm run test:e2e`
- `npm run check` (Biome check)
- `npm run lint` (Biome lint with write)
- `npm run format` (Biome format with write)
---
## Engineering Rules
### 1) Business Logic Ownership
- Keep domain rules in domain apps/services (`auth`, `bookings`, `tasks`, `notifications`).
- Keep `gateway` thin unless aggregation/proxy behavior is explicitly needed.
- Do not keep critical decisions only in controllers.
### 2) API Design
- Use clear route naming by domain.
- Keep DTOs explicit; avoid loosely typed payloads.
- Validate input on server side for all external entrypoints.
- Return consistent response shapes for similar endpoints.
### 3) Security
- Never hardcode secrets/tokens in source code.
- Read secrets from environment variables.
- Do not store plain passwords (use hashing once persistence is added).
- Treat auth/session flows as high-risk changes: require tests.
### 4) Data and Contracts
- Backend is canonical for entity rules and invariants.
- Public/widget-safe endpoints must not expose internal-only fields.
- Prefer stable identifiers for public integrations (`widgetKey`, slug, etc.), not internal DB IDs where avoidable.
### 5) Error Handling
- Use HTTP exceptions intentionally with clear status codes.
- Avoid swallowing upstream errors.
- Keep error messages useful but do not leak secrets/internal stack details to clients.
### 6) Code Quality
- Language: TypeScript.
- Keep modules focused by domain.
- Prefer readable code over clever abstractions.
- Use Biome as the only formatter/linter in this project.
---
## Migration Notes (Old Backend)
Legacy code exists in `../old/monie-backend`.
When migrating:
- migrate by domain, not by mass copy;
- remove deprecated scaffolding and sample-only code;
- do not copy insecure placeholders (e.g., hardcoded JWT secrets);
- adapt old code to current monorepo structure and Biome formatting.
Suggested migration order:
1. `auth` baseline (login/logout/profile + guards/roles)
2. `bookings` public read endpoints needed by widget/web
3. `tasks` and `notifications`
4. gateway aggregation behavior (if still needed)
---
## Testing Expectations
For any non-trivial change:
- add or update unit tests for changed services/controllers;
- run `npm run test`;
- run `npm run test:e2e` when routes/bootstrapping are touched.
For auth, booking rules, and access-control changes, tests are mandatory.
---
## Done Criteria
A backend task is considered done when:
1. scope is limited to required app/module(s);
2. API behavior is validated (tests and/or manual endpoint check);
3. `npm run check` passes;
4. `npm run build` passes;
5. no secrets or temporary hardcoded sensitive values are introduced.
---
## Commit Convention
Use Conventional Commits with backend scope when useful:
- `feat(backend): ...`
- `fix(backend): ...`
- `refactor(backend): ...`
- `test(backend): ...`
- `chore(backend): ...`
If change is isolated to a backend app, scope can be more specific:
- `feat(auth): ...`
- `feat(bookings): ...`
- `fix(gateway): ...`

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM node:24-bookworm-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:24-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3001
CMD ["npm", "run", "start:prod"]

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
describe('AuthController', () => {
let authController: AuthController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [AuthService],
}).compile();
authController = app.get<AuthController>(AuthController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(authController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get()
getHello(): string {
return this.authService.getHello();
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
imports: [],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class AppService { export class AuthService {
getHello(): string { getHello(): string {
return 'Hello World!'; return 'Hello World!';
} }

8
apps/auth/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AuthModule } from './auth.module';
async function bootstrap() {
const app = await NestFactory.create(AuthModule);
await app.listen(process.env.AUTH_PORT ?? 3000);
}
bootstrap();

View File

@@ -0,0 +1,21 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { AuthModule } from './../src/auth.module';
describe('AuthController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AuthModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/auth"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BookingsController } from './bookings.controller';
import { BookingsService } from './bookings.service';
describe('BookingsController', () => {
let bookingsController: BookingsController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [BookingsController],
providers: [BookingsService],
}).compile();
bookingsController = app.get<BookingsController>(BookingsController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(bookingsController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { BookingsService } from './bookings.service';
@Controller()
export class BookingsController {
constructor(private readonly bookingsService: BookingsService) {}
@Get()
getHello(): string {
return this.bookingsService.getHello();
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { BookingsController } from './bookings.controller';
import { BookingsService } from './bookings.service';
@Module({
imports: [],
controllers: [BookingsController],
providers: [BookingsService],
})
export class BookingsModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class BookingsService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { BookingsModule } from './bookings.module';
async function bootstrap() {
const app = await NestFactory.create(BookingsModule);
await app.listen(process.env.BOOKING_PORT ?? 3004);
}
bootstrap();

View File

@@ -0,0 +1,21 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { BookingsModule } from './../src/bookings.module';
describe('BookingsController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [BookingsModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/bookings"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GatewayController } from './gateway.controller';
import { GatewayService } from './gateway.service';
describe('GatewayController', () => {
let gatewayController: GatewayController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [GatewayController],
providers: [GatewayService],
}).compile();
gatewayController = app.get<GatewayController>(GatewayController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(gatewayController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { GatewayService } from './gateway.service';
@Controller()
export class GatewayController {
constructor(private readonly gatewayService: GatewayService) {}
@Get()
getHello(): string {
return this.gatewayService.getHello();
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { GatewayController } from './gateway.controller';
import { GatewayService } from './gateway.service';
@Module({
imports: [],
controllers: [GatewayController],
providers: [GatewayService],
})
export class GatewayModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class GatewayService {
getHello(): string {
return 'Hello World!';
}
}

8
apps/gateway/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { GatewayModule } from './gateway.module';
async function bootstrap() {
const app = await NestFactory.create(GatewayModule);
await app.listen(process.env.GATEWAY_PORT ?? 3001);
}
bootstrap();

View File

@@ -1,15 +1,14 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest'; import request from 'supertest';
import { App } from 'supertest/types'; import { GatewayModule } from './../src/gateway.module';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => { describe('GatewayController (e2e)', () => {
let app: INestApplication<App>; let app: INestApplication;
beforeEach(async () => { beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({ const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule], imports: [GatewayModule],
}).compile(); }).compile();
app = moduleFixture.createNestApplication(); app = moduleFixture.createNestApplication();
@@ -17,13 +16,6 @@ describe('AppController (e2e)', () => {
}); });
it('/ (GET)', () => { it('/ (GET)', () => {
return request(app.getHttpServer()) return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
.get('/')
.expect(200)
.expect('Hello World!');
});
afterEach(async () => {
await app.close();
}); });
}); });

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/gateway"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { NotificationsModule } from './notifications.module';
async function bootstrap() {
const app = await NestFactory.create(NotificationsModule);
await app.listen(process.env.NOTIFICATIONS_PORT ?? 3003);
}
bootstrap();

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './notifications.service';
describe('NotificationsController', () => {
let notificationsController: NotificationsController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [NotificationsController],
providers: [NotificationsService],
}).compile();
notificationsController = app.get<NotificationsController>(NotificationsController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(notificationsController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { NotificationsService } from './notifications.service';
@Controller()
export class NotificationsController {
constructor(private readonly notificationsService: NotificationsService) {}
@Get()
getHello(): string {
return this.notificationsService.getHello();
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './notifications.service';
@Module({
imports: [],
controllers: [NotificationsController],
providers: [NotificationsService],
})
export class NotificationsModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class NotificationsService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,21 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { NotificationsModule } from './../src/notifications.module';
describe('NotificationsController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [NotificationsModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/notifications"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

8
apps/tasks/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { TasksModule } from './tasks.module';
async function bootstrap() {
const app = await NestFactory.create(TasksModule);
await app.listen(process.env.TASKS_PORT ?? 3002);
}
bootstrap();

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
describe('TasksController', () => {
let tasksController: TasksController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [TasksController],
providers: [TasksService],
}).compile();
tasksController = app.get<TasksController>(TasksController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(tasksController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { TasksService } from './tasks.service';
@Controller()
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
getHello(): string {
return this.tasksService.getHello();
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
@Module({
imports: [],
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class TasksService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,21 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { TasksModule } from './../src/tasks.module';
describe('TasksController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TasksModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/tasks"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

47
biome.json Normal file
View File

@@ -0,0 +1,47 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"files": {
"includes": ["**", "!dist", "!coverage", "!node_modules"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useImportType": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all"
},
"parser": {
"unsafeParameterDecoratorsEnabled": true
},
"globals": [
"describe",
"it",
"expect",
"beforeEach",
"afterEach",
"beforeAll",
"afterAll",
"jest"
]
}
}

3
commitlint.config.mjs Normal file
View File

@@ -0,0 +1,3 @@
export default {
extends: ['@commitlint/config-conventional'],
};

View File

@@ -1,35 +0,0 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);

View File

@@ -1,8 +1,59 @@
{ {
"$schema": "https://json.schemastore.org/nest-cli", "$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "apps/gateway/src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true "deleteOutDir": true,
"webpack": true,
"tsConfigPath": "apps/gateway/tsconfig.app.json"
},
"monorepo": true,
"root": "apps/gateway",
"projects": {
"auth": {
"type": "application",
"root": "apps/auth",
"entryFile": "main",
"sourceRoot": "apps/auth/src",
"compilerOptions": {
"tsConfigPath": "apps/auth/tsconfig.app.json"
}
},
"bookings": {
"type": "application",
"root": "apps/bookings",
"entryFile": "main",
"sourceRoot": "apps/bookings/src",
"compilerOptions": {
"tsConfigPath": "apps/bookings/tsconfig.app.json"
}
},
"gateway": {
"type": "application",
"root": "apps/gateway",
"entryFile": "main",
"sourceRoot": "apps/gateway/src",
"compilerOptions": {
"tsConfigPath": "apps/gateway/tsconfig.app.json"
}
},
"notifications": {
"type": "application",
"root": "apps/notifications",
"entryFile": "main",
"sourceRoot": "apps/notifications/src",
"compilerOptions": {
"tsConfigPath": "apps/notifications/tsconfig.app.json"
}
},
"tasks": {
"type": "application",
"root": "apps/tasks",
"entryFile": "main",
"sourceRoot": "apps/tasks/src",
"compilerOptions": {
"tsConfigPath": "apps/tasks/tsconfig.app.json"
}
}
} }
} }

1906
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,24 @@
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "biome format --write .",
"start": "nest start", "start:auth": "nest start auth --watch",
"start:dev": "nest start --watch", "start:gateway": "nest start gateway --watch",
"start:debug": "nest start --debug --watch", "start:bookings": "nest start bookings --watch",
"start:prod": "node dist/main", "start:tasks": "nest start tasks --watch",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "start:notifications": "nest start notifications --watch",
"start": "nest start gateway",
"start:dev": "concurrently \"npm run start:auth\" \"npm run start:gateway\" \"npm run start:bookings\" \"npm run start:tasks\" \"npm run start:notifications\"",
"start:debug": "nest start gateway --debug --watch",
"start:prod": "node dist/apps/gateway/main",
"lint": "biome lint --write .",
"check": "biome check .",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./apps/gateway/test/jest-e2e.json",
"prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
@@ -27,8 +34,9 @@
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@biomejs/biome": "^2.4.5",
"@eslint/js": "^9.18.0", "@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@nestjs/cli": "^11.0.0", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
@@ -36,20 +44,16 @@
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@types/supertest": "^7.0.0", "@types/supertest": "^7.0.0",
"eslint": "^9.18.0", "concurrently": "^9.2.1",
"eslint-config-prettier": "^10.0.1", "husky": "^9.1.7",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^17.0.0",
"jest": "^30.0.0", "jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3", "typescript": "^5.7.3"
"typescript-eslint": "^8.20.0"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [
@@ -57,7 +61,7 @@
"json", "json",
"ts" "ts"
], ],
"rootDir": "src", "rootDir": ".",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
@@ -65,7 +69,10 @@
"collectCoverageFrom": [ "collectCoverageFrom": [
"**/*.(t|j)s" "**/*.(t|j)s"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "./coverage",
"testEnvironment": "node" "testEnvironment": "node",
"roots": [
"<rootDir>/apps/"
]
} }
} }

View File

@@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@@ -1,8 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -20,6 +20,7 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,
"noFallthroughCasesInSwitch": false "noFallthroughCasesInSwitch": false,
"paths": {}
} }
} }