@trinacria/cli 0.1.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @trinacria/cli might be problematic. Click here for more details.
- package/README.md +109 -0
- package/README.npm.md +109 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +88 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +102 -0
- package/dist/commands/dev.d.ts +28 -0
- package/dist/commands/dev.js +186 -0
- package/dist/commands/new.d.ts +41 -0
- package/dist/commands/new.js +280 -0
- package/dist/commands/start.d.ts +13 -0
- package/dist/commands/start.js +110 -0
- package/dist/config/config.contract.d.ts +9 -0
- package/dist/config/config.contract.js +2 -0
- package/dist/config/default-config.d.ts +2 -0
- package/dist/config/default-config.js +11 -0
- package/dist/config/load-config.d.ts +2 -0
- package/dist/config/load-config.js +113 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/templates/api-events-rabbitmq/.env.example +23 -0
- package/dist/templates/api-events-rabbitmq/Dockerfile +18 -0
- package/dist/templates/api-events-rabbitmq/README.md +97 -0
- package/dist/templates/api-events-rabbitmq/docker-compose.yml +30 -0
- package/dist/templates/api-events-rabbitmq/package.json +21 -0
- package/dist/templates/api-events-rabbitmq/src/global/config.service.ts +95 -0
- package/dist/templates/api-events-rabbitmq/src/global/controllers/swagger/docs.html +28 -0
- package/dist/templates/api-events-rabbitmq/src/global/controllers/swagger/swagger-docs.controller.ts +79 -0
- package/dist/templates/api-events-rabbitmq/src/global/rabbitmq.service.ts +86 -0
- package/dist/templates/api-events-rabbitmq/src/global/register-global-controllers.ts +20 -0
- package/dist/templates/api-events-rabbitmq/src/main.ts +89 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/dto/event.dto.ts +24 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/dto/index.ts +2 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/dto/publish-event.dto.ts +12 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/events.controller.ts +76 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/events.module.ts +27 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/events.provider.ts +28 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/events.service.ts +19 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/events.store.ts +42 -0
- package/dist/templates/api-events-rabbitmq/src/modules/events/events.tokens.ts +11 -0
- package/dist/templates/api-events-rabbitmq/trinacria.config.mjs +12 -0
- package/dist/templates/api-events-rabbitmq/tsconfig.json +14 -0
- package/dist/templates/api-events-redis/.env.example +18 -0
- package/dist/templates/api-events-redis/Dockerfile +18 -0
- package/dist/templates/api-events-redis/README.md +97 -0
- package/dist/templates/api-events-redis/docker-compose.yml +33 -0
- package/dist/templates/api-events-redis/package.json +18 -0
- package/dist/templates/api-events-redis/src/global/config.service.ts +93 -0
- package/dist/templates/api-events-redis/src/global/controllers/swagger/docs.html +28 -0
- package/dist/templates/api-events-redis/src/global/controllers/swagger/swagger-docs.controller.ts +79 -0
- package/dist/templates/api-events-redis/src/global/redis.service.ts +50 -0
- package/dist/templates/api-events-redis/src/global/register-global-controllers.ts +20 -0
- package/dist/templates/api-events-redis/src/main.ts +88 -0
- package/dist/templates/api-events-redis/src/modules/events/dto/event.dto.ts +24 -0
- package/dist/templates/api-events-redis/src/modules/events/dto/index.ts +2 -0
- package/dist/templates/api-events-redis/src/modules/events/dto/publish-event.dto.ts +12 -0
- package/dist/templates/api-events-redis/src/modules/events/events.controller.ts +76 -0
- package/dist/templates/api-events-redis/src/modules/events/events.module.ts +27 -0
- package/dist/templates/api-events-redis/src/modules/events/events.provider.ts +28 -0
- package/dist/templates/api-events-redis/src/modules/events/events.service.ts +19 -0
- package/dist/templates/api-events-redis/src/modules/events/events.store.ts +42 -0
- package/dist/templates/api-events-redis/src/modules/events/events.tokens.ts +11 -0
- package/dist/templates/api-events-redis/trinacria.config.mjs +12 -0
- package/dist/templates/api-events-redis/tsconfig.json +14 -0
- package/dist/templates/api-mongoose-mongodb/.env.example +17 -0
- package/dist/templates/api-mongoose-mongodb/Dockerfile +18 -0
- package/dist/templates/api-mongoose-mongodb/README.md +98 -0
- package/dist/templates/api-mongoose-mongodb/docker-compose.yml +34 -0
- package/dist/templates/api-mongoose-mongodb/package.json +17 -0
- package/dist/templates/api-mongoose-mongodb/src/global/config.service.ts +92 -0
- package/dist/templates/api-mongoose-mongodb/src/global/controllers/swagger/docs.html +28 -0
- package/dist/templates/api-mongoose-mongodb/src/global/controllers/swagger/swagger-docs.controller.ts +79 -0
- package/dist/templates/api-mongoose-mongodb/src/global/mongoose-schema.provider.ts +18 -0
- package/dist/templates/api-mongoose-mongodb/src/global/mongoose.service.ts +36 -0
- package/dist/templates/api-mongoose-mongodb/src/global/register-global-controllers.ts +20 -0
- package/dist/templates/api-mongoose-mongodb/src/main.ts +70 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/dto/create-user.dto.ts +14 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/dto/index.ts +2 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/dto/public-user.dto.ts +22 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/users.controller.ts +89 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/users.module.ts +17 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/users.schema.ts +35 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/users.service.ts +35 -0
- package/dist/templates/api-mongoose-mongodb/src/modules/users/users.tokens.ts +9 -0
- package/dist/templates/api-mongoose-mongodb/trinacria.config.mjs +12 -0
- package/dist/templates/api-mongoose-mongodb/tsconfig.json +14 -0
- package/dist/templates/api-prisma-postgresql/.env.example +19 -0
- package/dist/templates/api-prisma-postgresql/Dockerfile +24 -0
- package/dist/templates/api-prisma-postgresql/README.md +107 -0
- package/dist/templates/api-prisma-postgresql/docker-compose.yml +38 -0
- package/dist/templates/api-prisma-postgresql/package.json +26 -0
- package/dist/templates/api-prisma-postgresql/prisma/schema.prisma +15 -0
- package/dist/templates/api-prisma-postgresql/prisma.config.ts +9 -0
- package/dist/templates/api-prisma-postgresql/src/global/config.service.ts +92 -0
- package/dist/templates/api-prisma-postgresql/src/global/controllers/swagger/docs.html +28 -0
- package/dist/templates/api-prisma-postgresql/src/global/controllers/swagger/swagger-docs.controller.ts +79 -0
- package/dist/templates/api-prisma-postgresql/src/global/prisma.service.ts +20 -0
- package/dist/templates/api-prisma-postgresql/src/global/register-global-controllers.ts +20 -0
- package/dist/templates/api-prisma-postgresql/src/main.ts +70 -0
- package/dist/templates/api-prisma-postgresql/src/modules/users/dto/create-user.dto.ts +14 -0
- package/dist/templates/api-prisma-postgresql/src/modules/users/dto/index.ts +2 -0
- package/dist/templates/api-prisma-postgresql/src/modules/users/dto/public-user.dto.ts +22 -0
- package/dist/templates/api-prisma-postgresql/src/modules/users/users.controller.ts +89 -0
- package/dist/templates/api-prisma-postgresql/src/modules/users/users.module.ts +15 -0
- package/dist/templates/api-prisma-postgresql/src/modules/users/users.service.ts +36 -0
- package/dist/templates/api-prisma-postgresql/src/modules/users/users.tokens.ts +7 -0
- package/dist/templates/api-prisma-postgresql/trinacria.config.mjs +12 -0
- package/dist/templates/api-prisma-postgresql/tsconfig.json +14 -0
- package/dist/templates/app-starter/.env.example +1 -0
- package/dist/templates/app-starter/Dockerfile +13 -0
- package/dist/templates/app-starter/README.md +32 -0
- package/dist/templates/app-starter/docker-compose.yml +8 -0
- package/dist/templates/app-starter/package.json +14 -0
- package/dist/templates/app-starter/src/main.ts +14 -0
- package/dist/templates/app-starter/trinacria.config.mjs +12 -0
- package/dist/templates/app-starter/tsconfig.json +14 -0
- package/dist/templates/cron-example/.env.example +11 -0
- package/dist/templates/cron-example/Dockerfile +13 -0
- package/dist/templates/cron-example/README.md +68 -0
- package/dist/templates/cron-example/docker-compose.yml +11 -0
- package/dist/templates/cron-example/package.json +15 -0
- package/dist/templates/cron-example/src/config.service.ts +46 -0
- package/dist/templates/cron-example/src/main.ts +41 -0
- package/dist/templates/cron-example/src/modules/cron/cron.module.ts +15 -0
- package/dist/templates/cron-example/src/modules/cron/cron.tokens.ts +6 -0
- package/dist/templates/cron-example/src/modules/cron/example-cron-jobs.provider.ts +48 -0
- package/dist/templates/cron-example/trinacria.config.mjs +12 -0
- package/dist/templates/cron-example/tsconfig.json +14 -0
- package/package.json +32 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# API Example: RabbitMQ Events
|
|
2
|
+
|
|
3
|
+
A minimal API-only example using Trinacria Events with RabbitMQ transport.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This app is intentionally a base template:
|
|
8
|
+
|
|
9
|
+
- no authentication (no JWT/session/password flow)
|
|
10
|
+
- no authorization roles/policies
|
|
11
|
+
- no database persistence for domain data
|
|
12
|
+
- focus only on event publish/consume flow via RabbitMQ
|
|
13
|
+
|
|
14
|
+
## Endpoints
|
|
15
|
+
|
|
16
|
+
- `GET /health`
|
|
17
|
+
- `GET /events`
|
|
18
|
+
- `POST /events/publish`
|
|
19
|
+
- `GET /openapi.json`
|
|
20
|
+
- `GET /docs` (Swagger UI)
|
|
21
|
+
|
|
22
|
+
## Built-in middleware enabled
|
|
23
|
+
|
|
24
|
+
- `requestId`
|
|
25
|
+
- `requestLogger`
|
|
26
|
+
- `cors`
|
|
27
|
+
- `rateLimit`
|
|
28
|
+
- `requestTimeout`
|
|
29
|
+
- `securityHeaders`
|
|
30
|
+
|
|
31
|
+
## First steps (local)
|
|
32
|
+
|
|
33
|
+
1. Install dependencies from the monorepo root:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
2. Copy the environment file:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cp apps/api-events-rabbitmq/.env.example apps/api-events-rabbitmq/.env.development
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Make sure RabbitMQ is running locally and matches `RABBITMQ_URL`.
|
|
46
|
+
|
|
47
|
+
4. Start the API:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm run dev -w api-events-rabbitmq
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
5. Verify the app is running:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
curl http://127.0.0.1:4004/health
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Publish your first event
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
curl -X POST http://127.0.0.1:4004/events/publish \
|
|
63
|
+
-H 'content-type: application/json' \
|
|
64
|
+
-d '{"message":"hello rabbitmq events"}'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then list consumed events:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl http://127.0.0.1:4004/events
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Docker quick start
|
|
74
|
+
|
|
75
|
+
1. Create the Docker env file from the same template:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cp apps/api-events-rabbitmq/.env.example apps/api-events-rabbitmq/.env
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. Edit the Docker section in `apps/api-events-rabbitmq/.env` if you want custom RabbitMQ ports/queue setup.
|
|
82
|
+
|
|
83
|
+
3. Start RabbitMQ + API using that `.env` file:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
docker compose \
|
|
87
|
+
--env-file apps/api-events-rabbitmq/.env \
|
|
88
|
+
-f apps/api-events-rabbitmq/docker-compose.yml \
|
|
89
|
+
up --build
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The API will be available at `http://127.0.0.1:${DOCKER_API_PORT}` (default `4004`).
|
|
93
|
+
|
|
94
|
+
Compose services:
|
|
95
|
+
|
|
96
|
+
- `rabbitmq`: RabbitMQ 3 (management image)
|
|
97
|
+
- `api`: this Trinacria API
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
services:
|
|
2
|
+
rabbitmq:
|
|
3
|
+
image: rabbitmq:3.13-management-alpine
|
|
4
|
+
ports:
|
|
5
|
+
- "${DOCKER_RABBITMQ_PORT}:5672"
|
|
6
|
+
- "${DOCKER_RABBITMQ_MANAGEMENT_PORT}:15672"
|
|
7
|
+
healthcheck:
|
|
8
|
+
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
|
|
9
|
+
interval: 5s
|
|
10
|
+
timeout: 5s
|
|
11
|
+
retries: 20
|
|
12
|
+
|
|
13
|
+
api:
|
|
14
|
+
build:
|
|
15
|
+
context: ../..
|
|
16
|
+
dockerfile: apps/api-events-rabbitmq/Dockerfile
|
|
17
|
+
environment:
|
|
18
|
+
ENV: development
|
|
19
|
+
HOST: 0.0.0.0
|
|
20
|
+
PORT: 4004
|
|
21
|
+
OPENAPI_ENABLED: true
|
|
22
|
+
RABBITMQ_URL: ${DOCKER_RABBITMQ_URL}
|
|
23
|
+
RABBITMQ_EXCHANGE: ${DOCKER_RABBITMQ_EXCHANGE}
|
|
24
|
+
RABBITMQ_QUEUE_NAME: ${DOCKER_RABBITMQ_QUEUE_NAME}
|
|
25
|
+
RABBITMQ_ROUTING_PATTERN: ${DOCKER_RABBITMQ_ROUTING_PATTERN}
|
|
26
|
+
ports:
|
|
27
|
+
- "${DOCKER_API_PORT}:4004"
|
|
28
|
+
depends_on:
|
|
29
|
+
rabbitmq:
|
|
30
|
+
condition: service_healthy
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api-events-rabbitmq",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "node ../../packages/cli/dist/index.js start",
|
|
7
|
+
"build": "node ../../packages/cli/dist/index.js build",
|
|
8
|
+
"dev": "node ../../packages/cli/dist/index.js dev"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@trinacria/cli": "*",
|
|
12
|
+
"@trinacria/core": "*",
|
|
13
|
+
"@trinacria/events": "*",
|
|
14
|
+
"@trinacria/http": "*",
|
|
15
|
+
"@trinacria/schema": "*",
|
|
16
|
+
"amqplib": "^0.10.9"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/amqplib": "^0.10.8"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createToken } from "@trinacria/core";
|
|
4
|
+
import {
|
|
5
|
+
formatValidationError,
|
|
6
|
+
Infer,
|
|
7
|
+
s,
|
|
8
|
+
ValidationError,
|
|
9
|
+
} from "@trinacria/schema";
|
|
10
|
+
|
|
11
|
+
export const configSchema = s.object({
|
|
12
|
+
ENV: s.enum(["development", "staging", "production"]).default("development"),
|
|
13
|
+
PORT: s.number({ coerce: true, int: true, min: 1, max: 65535 }).default(4004),
|
|
14
|
+
HOST: s.string().default("127.0.0.1"),
|
|
15
|
+
OPENAPI_ENABLED: s.boolean({ coerce: true }).default(true),
|
|
16
|
+
CORS_ALLOWED_ORIGINS: s.array(s.string(), { coerce: true }).default([]),
|
|
17
|
+
RABBITMQ_URL: s.string(),
|
|
18
|
+
RABBITMQ_EXCHANGE: s.string().default("trinacria.events"),
|
|
19
|
+
RABBITMQ_QUEUE_NAME: s.string().default("api-events-rabbitmq.events"),
|
|
20
|
+
RABBITMQ_ROUTING_PATTERN: s.string().default("#"),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type ConfigModel = Infer<typeof configSchema>;
|
|
24
|
+
export const CONFIG_SERVICE = createToken<ConfigService>("CONFIG_SERVICE");
|
|
25
|
+
|
|
26
|
+
export class ConfigService {
|
|
27
|
+
private readonly config: ConfigModel;
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
this.loadEnvFile();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
this.config = configSchema.parse(process.env);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof ValidationError) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
formatValidationError(error, {
|
|
38
|
+
prefix: "Invalid environment configuration:",
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private loadEnvFile() {
|
|
48
|
+
const currentEnv = process.env.ENV || "development";
|
|
49
|
+
const fileNames = [".env", `.env.${currentEnv}`];
|
|
50
|
+
const roots = [
|
|
51
|
+
process.cwd(),
|
|
52
|
+
path.join(process.cwd(), "apps/api-events-rabbitmq"),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
fileNames.forEach((fileName) => {
|
|
56
|
+
roots.forEach((root) => {
|
|
57
|
+
const filePath = path.join(root, fileName);
|
|
58
|
+
if (!fs.existsSync(filePath)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
63
|
+
data.split("\n").forEach((line) => {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
70
|
+
if (separatorIndex === -1) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
75
|
+
const rawValue = trimmed.slice(separatorIndex + 1).trim();
|
|
76
|
+
const value =
|
|
77
|
+
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
|
78
|
+
(rawValue.startsWith("'") && rawValue.endsWith("'"))
|
|
79
|
+
? rawValue.slice(1, -1)
|
|
80
|
+
: rawValue;
|
|
81
|
+
|
|
82
|
+
process.env[key] = value;
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get<K extends keyof ConfigModel>(key: K): ConfigModel[K] {
|
|
89
|
+
return this.config[key];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getAll(): ConfigModel {
|
|
93
|
+
return this.config;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>API Events RabbitMQ Docs</title>
|
|
7
|
+
<link
|
|
8
|
+
rel="stylesheet"
|
|
9
|
+
href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"
|
|
10
|
+
/>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="swagger-ui"></div>
|
|
14
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
15
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
|
16
|
+
<script>
|
|
17
|
+
window.ui = SwaggerUIBundle({
|
|
18
|
+
url: "/openapi.json",
|
|
19
|
+
dom_id: "#swagger-ui",
|
|
20
|
+
deepLinking: true,
|
|
21
|
+
tryItOutEnabled: true,
|
|
22
|
+
persistAuthorization: true,
|
|
23
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
24
|
+
layout: "StandaloneLayout",
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
package/dist/templates/api-events-rabbitmq/src/global/controllers/swagger/swagger-docs.controller.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createToken } from "@trinacria/core";
|
|
2
|
+
import { HttpController, response } from "@trinacria/http";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { ConfigService } from "../../config.service";
|
|
6
|
+
|
|
7
|
+
export const SWAGGER_DOCS_CONTROLLER = createToken<SwaggerDocsController>(
|
|
8
|
+
"SWAGGER_DOCS_CONTROLLER",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export class SwaggerDocsController extends HttpController {
|
|
12
|
+
constructor(private readonly config: ConfigService) {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
routes() {
|
|
17
|
+
return this.router()
|
|
18
|
+
.get("/docs", this.renderDocs, {
|
|
19
|
+
docs: {
|
|
20
|
+
excludeFromOpenApi: true,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
.build();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async renderDocs() {
|
|
27
|
+
if (!this.config.get("OPENAPI_ENABLED")) {
|
|
28
|
+
return response({ message: "OpenAPI is disabled" }, { status: 404 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return response(await readSwaggerHtml(), {
|
|
32
|
+
headers: {
|
|
33
|
+
"content-type": "text/html; charset=utf-8",
|
|
34
|
+
"content-security-policy": docsContentSecurityPolicy(),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let cachedSwaggerHtml: string | undefined;
|
|
41
|
+
|
|
42
|
+
async function readSwaggerHtml(): Promise<string> {
|
|
43
|
+
if (cachedSwaggerHtml) {
|
|
44
|
+
return cachedSwaggerHtml;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const candidates = [
|
|
48
|
+
path.resolve(process.cwd(), "src/global/controllers/swagger/docs.html"),
|
|
49
|
+
path.resolve(
|
|
50
|
+
process.cwd(),
|
|
51
|
+
"apps/api-events-rabbitmq/src/global/controllers/swagger/docs.html",
|
|
52
|
+
),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const filePath of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
cachedSwaggerHtml = await fs.readFile(filePath, "utf8");
|
|
58
|
+
return cachedSwaggerHtml;
|
|
59
|
+
} catch {
|
|
60
|
+
// keep trying next candidate
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error("Swagger docs HTML template not found.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function docsContentSecurityPolicy(): string {
|
|
68
|
+
return [
|
|
69
|
+
"default-src 'self'",
|
|
70
|
+
"base-uri 'self'",
|
|
71
|
+
"object-src 'none'",
|
|
72
|
+
"frame-ancestors 'none'",
|
|
73
|
+
"script-src 'self' 'unsafe-inline' https://unpkg.com",
|
|
74
|
+
"style-src 'self' 'unsafe-inline' https://unpkg.com",
|
|
75
|
+
"img-src 'self' data: blob:",
|
|
76
|
+
"font-src 'self' data: https://unpkg.com",
|
|
77
|
+
"connect-src 'self'",
|
|
78
|
+
].join("; ");
|
|
79
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createToken } from "@trinacria/core";
|
|
2
|
+
import type { RabbitMqChannelLike } from "@trinacria/events";
|
|
3
|
+
import { connect, type Channel, type ChannelModel } from "amqplib";
|
|
4
|
+
import type { ConfigService } from "./config.service";
|
|
5
|
+
|
|
6
|
+
export const RABBITMQ_SERVICE =
|
|
7
|
+
createToken<RabbitMqService>("RABBITMQ_SERVICE");
|
|
8
|
+
|
|
9
|
+
export class RabbitMqService {
|
|
10
|
+
private connection?: ChannelModel;
|
|
11
|
+
private channel?: Channel;
|
|
12
|
+
|
|
13
|
+
constructor(private readonly config: ConfigService) {}
|
|
14
|
+
|
|
15
|
+
async onInit(): Promise<void> {
|
|
16
|
+
if (!this.connection) {
|
|
17
|
+
this.connection = await connect(this.config.get("RABBITMQ_URL"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!this.channel) {
|
|
21
|
+
this.channel = await this.connection.createChannel();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getChannel(): Channel {
|
|
26
|
+
if (!this.channel) {
|
|
27
|
+
throw new Error("RabbitMQ channel is not initialized.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return this.channel;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getTransportChannel(): RabbitMqChannelLike {
|
|
34
|
+
const channel = this.getChannel();
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
assertExchange: async (exchange, type, options) => {
|
|
38
|
+
await channel.assertExchange(exchange, type, options);
|
|
39
|
+
},
|
|
40
|
+
assertQueue: (queue, options) => channel.assertQueue(queue, options),
|
|
41
|
+
bindQueue: async (queue, exchange, pattern) => {
|
|
42
|
+
await channel.bindQueue(queue, exchange, pattern);
|
|
43
|
+
},
|
|
44
|
+
unbindQueue: async (queue, exchange, pattern) => {
|
|
45
|
+
await channel.unbindQueue(queue, exchange, pattern);
|
|
46
|
+
},
|
|
47
|
+
consume: async (queue, onMessage, options) => {
|
|
48
|
+
const consumeOk = await channel.consume(
|
|
49
|
+
queue,
|
|
50
|
+
async (message) => {
|
|
51
|
+
await onMessage(message);
|
|
52
|
+
},
|
|
53
|
+
options,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return { consumerTag: consumeOk.consumerTag };
|
|
57
|
+
},
|
|
58
|
+
cancel: async (consumerTag) => {
|
|
59
|
+
await channel.cancel(consumerTag);
|
|
60
|
+
},
|
|
61
|
+
publish: (exchange, routingKey, content, options) =>
|
|
62
|
+
channel.publish(exchange, routingKey, content, options),
|
|
63
|
+
ack: (message) => {
|
|
64
|
+
channel.ack(message as any);
|
|
65
|
+
},
|
|
66
|
+
nack: (message, allUpTo, requeue) => {
|
|
67
|
+
channel.nack(message as any, allUpTo, requeue);
|
|
68
|
+
},
|
|
69
|
+
deleteQueue: async (queue) => {
|
|
70
|
+
await channel.deleteQueue(queue);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async onDestroy(): Promise<void> {
|
|
76
|
+
if (this.channel) {
|
|
77
|
+
await this.channel.close();
|
|
78
|
+
this.channel = undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.connection) {
|
|
82
|
+
await this.connection.close();
|
|
83
|
+
this.connection = undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TrinacriaApp } from "@trinacria/core";
|
|
2
|
+
import { httpProvider } from "@trinacria/http";
|
|
3
|
+
import { ConfigService, CONFIG_SERVICE } from "./config.service";
|
|
4
|
+
import {
|
|
5
|
+
SWAGGER_DOCS_CONTROLLER,
|
|
6
|
+
SwaggerDocsController,
|
|
7
|
+
} from "./controllers/swagger/swagger-docs.controller";
|
|
8
|
+
|
|
9
|
+
export function registerGlobalControllers(
|
|
10
|
+
app: TrinacriaApp,
|
|
11
|
+
config: ConfigService,
|
|
12
|
+
): void {
|
|
13
|
+
if (config.get("OPENAPI_ENABLED")) {
|
|
14
|
+
app.registerGlobalProvider(
|
|
15
|
+
httpProvider(SWAGGER_DOCS_CONTROLLER, SwaggerDocsController, [
|
|
16
|
+
CONFIG_SERVICE,
|
|
17
|
+
]),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ConsoleLogger, TrinacriaApp, valueProvider } from "@trinacria/core";
|
|
2
|
+
import { createEventsPlugin, RabbitMqEventTransport } from "@trinacria/events";
|
|
3
|
+
import {
|
|
4
|
+
cors,
|
|
5
|
+
createHttpPlugin,
|
|
6
|
+
createSecurityHeadersBuilder,
|
|
7
|
+
rateLimit,
|
|
8
|
+
requestId,
|
|
9
|
+
requestLogger,
|
|
10
|
+
requestTimeout,
|
|
11
|
+
} from "@trinacria/http";
|
|
12
|
+
import { CONFIG_SERVICE, ConfigService } from "./global/config.service";
|
|
13
|
+
import { RABBITMQ_SERVICE, RabbitMqService } from "./global/rabbitmq.service";
|
|
14
|
+
import { registerGlobalControllers } from "./global/register-global-controllers";
|
|
15
|
+
import { EventsModule } from "./modules/events/events.module";
|
|
16
|
+
|
|
17
|
+
async function bootstrap() {
|
|
18
|
+
const app = new TrinacriaApp();
|
|
19
|
+
const configService = new ConfigService();
|
|
20
|
+
const config = configService.getAll();
|
|
21
|
+
const eventsLogger = new ConsoleLogger("api-events-rabbitmq:events");
|
|
22
|
+
const isProduction = config.ENV === "production";
|
|
23
|
+
const corsOrigins = config.CORS_ALLOWED_ORIGINS;
|
|
24
|
+
const securityHeadersMiddleware = createSecurityHeadersBuilder()
|
|
25
|
+
.preset(config.ENV)
|
|
26
|
+
.trustProxy(false)
|
|
27
|
+
.build();
|
|
28
|
+
|
|
29
|
+
const rabbitMqService = new RabbitMqService(configService);
|
|
30
|
+
await rabbitMqService.onInit();
|
|
31
|
+
|
|
32
|
+
app.registerGlobalProvider(valueProvider(CONFIG_SERVICE, configService));
|
|
33
|
+
app.registerGlobalProvider(valueProvider(RABBITMQ_SERVICE, rabbitMqService));
|
|
34
|
+
registerGlobalControllers(app, configService);
|
|
35
|
+
|
|
36
|
+
app.use(
|
|
37
|
+
createHttpPlugin({
|
|
38
|
+
host: config.HOST,
|
|
39
|
+
port: config.PORT,
|
|
40
|
+
middlewares: [
|
|
41
|
+
requestId(),
|
|
42
|
+
requestLogger({ includeUserAgent: !isProduction }),
|
|
43
|
+
cors({
|
|
44
|
+
origin: corsOrigins.length > 0 ? corsOrigins : "*",
|
|
45
|
+
credentials: true,
|
|
46
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
|
|
47
|
+
}),
|
|
48
|
+
rateLimit({
|
|
49
|
+
windowMs: 60_000,
|
|
50
|
+
max: isProduction ? 240 : 2_000,
|
|
51
|
+
trustProxy: false,
|
|
52
|
+
}),
|
|
53
|
+
requestTimeout({ timeoutMs: 15_000 }),
|
|
54
|
+
securityHeadersMiddleware,
|
|
55
|
+
],
|
|
56
|
+
openApi: config.OPENAPI_ENABLED
|
|
57
|
+
? {
|
|
58
|
+
enabled: true,
|
|
59
|
+
title: "API Events RabbitMQ Example",
|
|
60
|
+
version: "1.0.0",
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
app.use(
|
|
67
|
+
createEventsPlugin({
|
|
68
|
+
transport: new RabbitMqEventTransport({
|
|
69
|
+
channel: rabbitMqService.getTransportChannel(),
|
|
70
|
+
exchange: config.RABBITMQ_EXCHANGE,
|
|
71
|
+
queueName: config.RABBITMQ_QUEUE_NAME,
|
|
72
|
+
routingPattern: config.RABBITMQ_ROUTING_PATTERN,
|
|
73
|
+
}),
|
|
74
|
+
source: "api-events-rabbitmq",
|
|
75
|
+
dispatchLocalOnEmit: false,
|
|
76
|
+
onListenerError: (error, envelope) => {
|
|
77
|
+
eventsLogger.error(`Event listener error: ${envelope.name}`, error);
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await app.registerModule(EventsModule);
|
|
83
|
+
await app.start();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
bootstrap().catch((error) => {
|
|
87
|
+
console.error(error);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { s } from "@trinacria/schema";
|
|
2
|
+
|
|
3
|
+
export interface EventDto {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
payload: {
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
receivedAt: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const EventDtoSchema = s.objectOf<EventDto>()(
|
|
13
|
+
{
|
|
14
|
+
id: s.string({ trim: true, minLength: 1 }),
|
|
15
|
+
name: s.string({ trim: true, minLength: 1 }),
|
|
16
|
+
payload: s.object({
|
|
17
|
+
message: s.string({ trim: true, minLength: 1 }),
|
|
18
|
+
}),
|
|
19
|
+
receivedAt: s.date(),
|
|
20
|
+
},
|
|
21
|
+
{ strict: true },
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const EventListDtoSchema = s.array(EventDtoSchema);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { s } from "@trinacria/schema";
|
|
2
|
+
|
|
3
|
+
export interface PublishEventDto {
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const PublishEventDtoSchema = s.objectOf<PublishEventDto>()(
|
|
8
|
+
{
|
|
9
|
+
message: s.string({ trim: true, minLength: 1 }),
|
|
10
|
+
},
|
|
11
|
+
{ strict: true },
|
|
12
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { HttpController, HttpContext, response } from "@trinacria/http";
|
|
2
|
+
import type { EventBus } from "@trinacria/events";
|
|
3
|
+
import { EventListDtoSchema, PublishEventDtoSchema } from "./dto";
|
|
4
|
+
import { DEMO_EVENT_NAME } from "./events.provider";
|
|
5
|
+
import { EventsService } from "./events.service";
|
|
6
|
+
|
|
7
|
+
export class EventsController extends HttpController {
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly eventsService: EventsService,
|
|
10
|
+
private readonly eventBus: EventBus,
|
|
11
|
+
) {
|
|
12
|
+
super();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
routes() {
|
|
16
|
+
return this.router()
|
|
17
|
+
.get("/health", this.healthCheck, {
|
|
18
|
+
docs: {
|
|
19
|
+
tags: ["Health"],
|
|
20
|
+
summary: "Health check",
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
.get("/events", this.listEvents, {
|
|
24
|
+
docs: {
|
|
25
|
+
tags: ["Events"],
|
|
26
|
+
summary: "List consumed events",
|
|
27
|
+
responses: {
|
|
28
|
+
200: {
|
|
29
|
+
description: "Consumed events",
|
|
30
|
+
schema: EventListDtoSchema.toOpenApi(),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
.post("/events/publish", this.publishEvent, {
|
|
36
|
+
docs: {
|
|
37
|
+
tags: ["Events"],
|
|
38
|
+
summary: "Publish demo event to RabbitMQ transport",
|
|
39
|
+
requestBody: {
|
|
40
|
+
required: true,
|
|
41
|
+
schema: PublishEventDtoSchema.toOpenApi(),
|
|
42
|
+
},
|
|
43
|
+
responses: {
|
|
44
|
+
202: {
|
|
45
|
+
description: "Event accepted",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
.build();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
healthCheck() {
|
|
54
|
+
return { status: "ok" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
listEvents() {
|
|
58
|
+
return this.eventsService.list();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async publishEvent(ctx: HttpContext) {
|
|
62
|
+
const payload = PublishEventDtoSchema.parse(ctx.body);
|
|
63
|
+
|
|
64
|
+
await this.eventBus.emit(DEMO_EVENT_NAME, {
|
|
65
|
+
message: payload.message,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return response(
|
|
69
|
+
{
|
|
70
|
+
status: "accepted",
|
|
71
|
+
event: DEMO_EVENT_NAME,
|
|
72
|
+
},
|
|
73
|
+
{ status: 202 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { classProvider, defineModule } from "@trinacria/core";
|
|
2
|
+
import { EVENT_BUS_TOKEN, eventProvider } from "@trinacria/events";
|
|
3
|
+
import { httpProvider } from "@trinacria/http";
|
|
4
|
+
import {
|
|
5
|
+
EVENTS_CONTROLLER,
|
|
6
|
+
EVENTS_PROVIDER,
|
|
7
|
+
EVENTS_SERVICE,
|
|
8
|
+
EVENTS_STORE,
|
|
9
|
+
} from "./events.tokens";
|
|
10
|
+
import { EventsController } from "./events.controller";
|
|
11
|
+
import { EventsProvider } from "./events.provider";
|
|
12
|
+
import { EventsService } from "./events.service";
|
|
13
|
+
import { EventsStore } from "./events.store";
|
|
14
|
+
|
|
15
|
+
export const EventsModule = defineModule({
|
|
16
|
+
name: "EventsModule",
|
|
17
|
+
providers: [
|
|
18
|
+
classProvider(EVENTS_STORE, EventsStore),
|
|
19
|
+
classProvider(EVENTS_SERVICE, EventsService, [EVENTS_STORE]),
|
|
20
|
+
httpProvider(EVENTS_CONTROLLER, EventsController, [
|
|
21
|
+
EVENTS_SERVICE,
|
|
22
|
+
EVENT_BUS_TOKEN,
|
|
23
|
+
]),
|
|
24
|
+
eventProvider(EVENTS_PROVIDER, EventsProvider, [EVENTS_SERVICE]),
|
|
25
|
+
],
|
|
26
|
+
exports: [EVENTS_SERVICE, EVENTS_CONTROLLER, EVENTS_PROVIDER],
|
|
27
|
+
});
|