@verdeect/kit 0.0.0-stage → 1.0.0-rc.2
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.
- package/CHANGELOG.md +138 -0
- package/README.md +804 -2
- package/alerts/verdeect-kit.rules.yml +133 -0
- package/dist/cli/commands/crypto.d.ts +9 -0
- package/dist/cli/commands/crypto.js +87 -0
- package/dist/cli/commands/db.d.ts +11 -0
- package/dist/cli/commands/db.js +151 -0
- package/dist/cli/commands/openapi.d.ts +9 -0
- package/dist/cli/commands/openapi.js +144 -0
- package/dist/cli/commands/rabbitmq.d.ts +11 -0
- package/dist/cli/commands/rabbitmq.js +124 -0
- package/dist/cli/commands/redis.d.ts +11 -0
- package/dist/cli/commands/redis.js +119 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/config/cli-schemas.d.ts +29 -0
- package/dist/config/cli-schemas.js +15 -0
- package/dist/config/define-env.d.ts +21 -0
- package/dist/config/define-env.js +43 -0
- package/dist/config/fragments.d.ts +105 -0
- package/dist/config/fragments.js +86 -0
- package/dist/config/helpers.d.ts +26 -0
- package/dist/config/helpers.js +62 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/config/kit-config.module.d.ts +20 -0
- package/dist/config/kit-config.module.js +23 -0
- package/dist/config/parse-env.d.ts +14 -0
- package/dist/config/parse-env.js +28 -0
- package/dist/crypto/cipher.d.ts +61 -0
- package/dist/crypto/cipher.js +136 -0
- package/dist/crypto/index.d.ts +2 -0
- package/dist/crypto/index.js +2 -0
- package/dist/database/batches.d.ts +42 -0
- package/dist/database/batches.js +49 -0
- package/dist/database/db.d.ts +93 -0
- package/dist/database/db.js +234 -0
- package/dist/database/if-newer.d.ts +9 -0
- package/dist/database/if-newer.js +15 -0
- package/dist/database/index.d.ts +17 -0
- package/dist/database/index.js +13 -0
- package/dist/database/kit-database.module.d.ts +47 -0
- package/dist/database/kit-database.module.js +181 -0
- package/dist/database/migrations.d.ts +42 -0
- package/dist/database/migrations.js +142 -0
- package/dist/database/pg.d.ts +20 -0
- package/dist/database/pg.js +19 -0
- package/dist/database/prepare-database.d.ts +39 -0
- package/dist/database/prepare-database.js +75 -0
- package/dist/database/run-exclusive.d.ts +19 -0
- package/dist/database/run-exclusive.js +62 -0
- package/dist/database/tenant.d.ts +34 -0
- package/dist/database/tenant.js +28 -0
- package/dist/http/api/api.decorator.d.ts +18 -0
- package/dist/http/api/api.decorator.js +9 -0
- package/dist/http/api/openapi-document.d.ts +40 -0
- package/dist/http/api/openapi-document.js +452 -0
- package/dist/http/bootstrap-service.d.ts +33 -0
- package/dist/http/bootstrap-service.js +107 -0
- package/dist/http/create-http-app.d.ts +38 -0
- package/dist/http/create-http-app.js +137 -0
- package/dist/http/errors.d.ts +118 -0
- package/dist/http/errors.js +94 -0
- package/dist/http/index.d.ts +12 -0
- package/dist/http/index.js +8 -0
- package/dist/http/kit-exception.filter.d.ts +18 -0
- package/dist/http/kit-exception.filter.js +151 -0
- package/dist/http/kit-http-core.module.d.ts +19 -0
- package/dist/http/kit-http-core.module.js +122 -0
- package/dist/http/pagination.d.ts +30 -0
- package/dist/http/pagination.js +73 -0
- package/dist/http/zod-validation.pipe.d.ts +15 -0
- package/dist/http/zod-validation.pipe.js +47 -0
- package/dist/idempotency/idempotency.interceptor.d.ts +55 -0
- package/dist/idempotency/idempotency.interceptor.js +317 -0
- package/dist/idempotency/idempotent.decorator.d.ts +7 -0
- package/dist/idempotency/idempotent.decorator.js +16 -0
- package/dist/idempotency/index.d.ts +7 -0
- package/dist/idempotency/index.js +6 -0
- package/dist/idempotency/kit-idempotency.module.d.ts +47 -0
- package/dist/idempotency/kit-idempotency.module.js +173 -0
- package/dist/idempotency/peer.d.ts +1 -0
- package/dist/idempotency/peer.js +4 -0
- package/dist/idempotency/table.d.ts +203 -0
- package/dist/idempotency/table.js +86 -0
- package/dist/internal/cleanup-result.d.ts +7 -0
- package/dist/internal/cleanup-result.js +12 -0
- package/dist/internal/ids.d.ts +13 -0
- package/dist/internal/ids.js +41 -0
- package/dist/internal/metadata.d.ts +12 -0
- package/dist/internal/metadata.js +3 -0
- package/dist/internal/outbox-policy.d.ts +8 -0
- package/dist/internal/outbox-policy.js +10 -0
- package/dist/internal/peer.d.ts +16 -0
- package/dist/internal/peer.js +56 -0
- package/dist/internal/pg-errors.d.ts +21 -0
- package/dist/internal/pg-errors.js +115 -0
- package/dist/internal/preparsed-env.d.ts +4 -0
- package/dist/internal/preparsed-env.js +14 -0
- package/dist/internal/rabbitmq-definitions.d.ts +85 -0
- package/dist/internal/rabbitmq-definitions.js +73 -0
- package/dist/internal/redis-acl.d.ts +21 -0
- package/dist/internal/redis-acl.js +30 -0
- package/dist/internal/service-name.d.ts +4 -0
- package/dist/internal/service-name.js +10 -0
- package/dist/internal/sql-identifier.d.ts +5 -0
- package/dist/internal/sql-identifier.js +12 -0
- package/dist/internal/tokens.d.ts +10 -0
- package/dist/internal/tokens.js +12 -0
- package/dist/internal/tx-context.d.ts +5 -0
- package/dist/internal/tx-context.js +9 -0
- package/dist/messaging/cleanup.d.ts +49 -0
- package/dist/messaging/cleanup.js +129 -0
- package/dist/messaging/config.d.ts +43 -0
- package/dist/messaging/config.js +8 -0
- package/dist/messaging/define-event.d.ts +29 -0
- package/dist/messaging/define-event.js +38 -0
- package/dist/messaging/envelope.d.ts +66 -0
- package/dist/messaging/envelope.js +49 -0
- package/dist/messaging/errors.d.ts +45 -0
- package/dist/messaging/errors.js +63 -0
- package/dist/messaging/inbox.d.ts +4 -0
- package/dist/messaging/inbox.js +9 -0
- package/dist/messaging/index.d.ts +13 -0
- package/dist/messaging/index.js +10 -0
- package/dist/messaging/kit-messaging.module.d.ts +39 -0
- package/dist/messaging/kit-messaging.module.js +309 -0
- package/dist/messaging/outbox-relay.d.ts +67 -0
- package/dist/messaging/outbox-relay.js +260 -0
- package/dist/messaging/outbox-store.d.ts +75 -0
- package/dist/messaging/outbox-store.js +126 -0
- package/dist/messaging/peer.d.ts +1 -0
- package/dist/messaging/peer.js +5 -0
- package/dist/messaging/rabbit-connection.d.ts +144 -0
- package/dist/messaging/rabbit-connection.js +472 -0
- package/dist/messaging/redelivery.d.ts +6 -0
- package/dist/messaging/redelivery.js +17 -0
- package/dist/messaging/subscriber.d.ts +102 -0
- package/dist/messaging/subscriber.js +412 -0
- package/dist/messaging/tables.d.ts +315 -0
- package/dist/messaging/tables.js +72 -0
- package/dist/messaging/topology.d.ts +56 -0
- package/dist/messaging/topology.js +56 -0
- package/dist/observability/fail-startup.d.ts +14 -0
- package/dist/observability/fail-startup.js +51 -0
- package/dist/observability/health-registry.d.ts +34 -0
- package/dist/observability/health-registry.js +81 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +10 -0
- package/dist/observability/lifecycle.d.ts +52 -0
- package/dist/observability/lifecycle.js +151 -0
- package/dist/observability/logger-options.d.ts +54 -0
- package/dist/observability/logger-options.js +91 -0
- package/dist/observability/metrics-server.d.ts +33 -0
- package/dist/observability/metrics-server.js +108 -0
- package/dist/observability/metrics.d.ts +30 -0
- package/dist/observability/metrics.js +53 -0
- package/dist/observability/redaction.d.ts +23 -0
- package/dist/observability/redaction.js +65 -0
- package/dist/observability/sanitize-error.d.ts +47 -0
- package/dist/observability/sanitize-error.js +194 -0
- package/dist/observability/sentry.d.ts +64 -0
- package/dist/observability/sentry.js +233 -0
- package/dist/redis/channels.d.ts +34 -0
- package/dist/redis/channels.js +128 -0
- package/dist/redis/client.d.ts +42 -0
- package/dist/redis/client.js +85 -0
- package/dist/redis/index.d.ts +7 -0
- package/dist/redis/index.js +7 -0
- package/dist/redis/kit-redis.module.d.ts +18 -0
- package/dist/redis/kit-redis.module.js +131 -0
- package/dist/redis/peer.d.ts +1 -0
- package/dist/redis/peer.js +4 -0
- package/dist/redis/rate-limiter.d.ts +49 -0
- package/dist/redis/rate-limiter.js +139 -0
- package/dist/testing/count-queries.d.ts +30 -0
- package/dist/testing/count-queries.js +58 -0
- package/dist/testing/create-test-app.d.ts +26 -0
- package/dist/testing/create-test-app.js +32 -0
- package/dist/testing/expect-matches-api.d.ts +8 -0
- package/dist/testing/expect-matches-api.js +49 -0
- package/dist/testing/expect-problem.d.ts +24 -0
- package/dist/testing/expect-problem.js +23 -0
- package/dist/testing/images.d.ts +10 -0
- package/dist/testing/images.js +14 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/launch-command.d.ts +52 -0
- package/dist/testing/launch-command.js +510 -0
- package/dist/testing/outbox-coverage.d.ts +26 -0
- package/dist/testing/outbox-coverage.js +98 -0
- package/dist/testing/rabbitmq-admin.d.ts +81 -0
- package/dist/testing/rabbitmq-admin.js +146 -0
- package/dist/testing/redis-worker.d.ts +10 -0
- package/dist/testing/redis-worker.js +15 -0
- package/dist/testing/repo-conventions.d.ts +10 -0
- package/dist/testing/repo-conventions.js +524 -0
- package/dist/testing/repo-files.d.ts +113 -0
- package/dist/testing/repo-files.js +401 -0
- package/dist/testing/scratch-database.d.ts +19 -0
- package/dist/testing/scratch-database.js +75 -0
- package/dist/testing/start-test-stack.d.ts +85 -0
- package/dist/testing/start-test-stack.js +342 -0
- package/dist/testing/tcp-proxy.d.ts +40 -0
- package/dist/testing/tcp-proxy.js +164 -0
- package/dist/testing/tenant-isolation.d.ts +31 -0
- package/dist/testing/tenant-isolation.js +205 -0
- package/dist/testing/test-events.d.ts +37 -0
- package/dist/testing/test-events.js +150 -0
- package/dist/testing/timeout-scale.d.ts +4 -0
- package/dist/testing/timeout-scale.js +17 -0
- package/dist/testing/timing-guard.d.ts +22 -0
- package/dist/testing/timing-guard.js +54 -0
- package/dist/testing/worker-scope.d.ts +31 -0
- package/dist/testing/worker-scope.js +68 -0
- package/package.json +177 -4
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// runMigrations, moveJournal (prd.md §3.3, AC-4.7, AC-4.17). Эталон — O migrator.ts: пул max 1, срок соединения 5 с,
|
|
2
|
+
// подсчёт применённых двумя запросами, транзакция миграций на выделенном клиенте. По PRD: журнал <schema>.__migrations,
|
|
3
|
+
// папка и схема — от сервиса; схема drizzle не создаётся.
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { assertServiceName } from '../internal/service-name.js';
|
|
7
|
+
import { quoteIdentifier } from '../internal/sql-identifier.js';
|
|
8
|
+
import { CONNECTION_TIMEOUT_MS, loadDrizzle, loadPg } from './pg.js';
|
|
9
|
+
/** Таблица журнала миграций в схеме сервиса. */
|
|
10
|
+
export const MIGRATIONS_TABLE = '__migrations';
|
|
11
|
+
/** Журнал drizzle-kit по умолчанию, который переносит `moveJournal`. */
|
|
12
|
+
const LEGACY_TABLE = '__drizzle_migrations';
|
|
13
|
+
/** Неверное использование (нет журнала drizzle-kit в папке); CLI переводит в код `USAGE`. */
|
|
14
|
+
export class KitUsageError extends Error {
|
|
15
|
+
name = 'KitUsageError';
|
|
16
|
+
}
|
|
17
|
+
function journalName(schema, table) {
|
|
18
|
+
return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
19
|
+
}
|
|
20
|
+
async function countApplied(pool, schema) {
|
|
21
|
+
const journal = journalName(schema, MIGRATIONS_TABLE);
|
|
22
|
+
// Две команды: PostgreSQL разбирает подзапрос до вычисления CASE — отсутствующая таблица даёт 42P01.
|
|
23
|
+
const exists = await pool.query('select to_regclass($1) is not null as exists', [journal]);
|
|
24
|
+
if (exists.rows[0]?.exists !== true) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const { rows } = await pool.query(`select count(*)::int as count from ${journal}`);
|
|
28
|
+
return rows[0]?.count ?? 0;
|
|
29
|
+
}
|
|
30
|
+
async function openPool(url) {
|
|
31
|
+
const pg = await loadPg();
|
|
32
|
+
const pool = new pg.Pool({
|
|
33
|
+
connectionString: url,
|
|
34
|
+
max: 1,
|
|
35
|
+
connectionTimeoutMillis: CONNECTION_TIMEOUT_MS,
|
|
36
|
+
});
|
|
37
|
+
// Обрыв простаивающего клиента: pg-pool генерирует 'error' на пуле — без слушателя процесс упал бы.
|
|
38
|
+
pool.on('error', () => undefined);
|
|
39
|
+
return pool;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Применяет SQL-миграции drizzle-kit из `folder` под ролью владельца; журнал — `<schema>.__migrations`.
|
|
43
|
+
* Повтор — `{applied: 0}`.
|
|
44
|
+
* @throws {TypeError} схема вне формата имени сервиса.
|
|
45
|
+
* @throws {KitUsageError} в папке нет `meta/_journal.json`.
|
|
46
|
+
*/
|
|
47
|
+
export async function runMigrations(options, logger) {
|
|
48
|
+
assertServiceName(options.schema, 'runMigrations');
|
|
49
|
+
if (!existsSync(join(options.folder, 'meta', '_journal.json'))) {
|
|
50
|
+
throw new KitUsageError(`runMigrations: нет журнала drizzle-kit в ${options.folder}`);
|
|
51
|
+
}
|
|
52
|
+
const { drizzle, migrate } = await loadDrizzle();
|
|
53
|
+
const pool = await openPool(options.url);
|
|
54
|
+
try {
|
|
55
|
+
const before = await countApplied(pool, options.schema);
|
|
56
|
+
logger?.debug({ schema: options.schema, before }, 'runMigrations: применение');
|
|
57
|
+
// Транзакция миграций — на выделенном клиенте: drizzle-orm 0.45.4 в transaction() над пулом выполняет BEGIN
|
|
58
|
+
// вне try/finally — при обрыве на BEGIN клиент не возвращался бы и pool.end() висел (эталон O).
|
|
59
|
+
const client = await pool.connect();
|
|
60
|
+
let failure;
|
|
61
|
+
try {
|
|
62
|
+
await migrate(drizzle(client), {
|
|
63
|
+
migrationsFolder: options.folder,
|
|
64
|
+
migrationsSchema: options.schema,
|
|
65
|
+
migrationsTable: MIGRATIONS_TABLE,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
client.release(failure);
|
|
74
|
+
}
|
|
75
|
+
const applied = (await countApplied(pool, options.schema)) - before;
|
|
76
|
+
logger?.info({ schema: options.schema, applied }, 'runMigrations: готово');
|
|
77
|
+
return { applied };
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await pool.end();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Переносит журнал `<from>.__drizzle_migrations` в `<to>.__migrations` (строки, которых нет по `hash`), удаляет старый
|
|
85
|
+
* журнал и пустую схему `<from>` — в одной транзакции. Повтор — `{moved: 0}`.
|
|
86
|
+
* @throws {TypeError} схема вне формата или `from === to`.
|
|
87
|
+
*/
|
|
88
|
+
export async function moveJournal(options, logger) {
|
|
89
|
+
assertServiceName(options.from, 'moveJournal');
|
|
90
|
+
assertServiceName(options.to, 'moveJournal');
|
|
91
|
+
if (options.from === options.to) {
|
|
92
|
+
throw new TypeError('moveJournal: схемы from и to совпадают');
|
|
93
|
+
}
|
|
94
|
+
const source = journalName(options.from, LEGACY_TABLE);
|
|
95
|
+
const target = journalName(options.to, MIGRATIONS_TABLE);
|
|
96
|
+
const pool = await openPool(options.url);
|
|
97
|
+
try {
|
|
98
|
+
const client = await pool.connect();
|
|
99
|
+
let failure;
|
|
100
|
+
try {
|
|
101
|
+
await client.query('BEGIN');
|
|
102
|
+
const exists = await client.query('select to_regclass($1) is not null as exists', [source]);
|
|
103
|
+
if (exists.rows[0]?.exists !== true) {
|
|
104
|
+
await client.query('COMMIT');
|
|
105
|
+
logger?.info({ from: options.from, to: options.to, moved: 0 }, 'moveJournal: готово');
|
|
106
|
+
return { moved: 0 };
|
|
107
|
+
}
|
|
108
|
+
await client.query(`CREATE TABLE IF NOT EXISTS ${target} (id SERIAL PRIMARY KEY, hash text NOT NULL, created_at bigint)`);
|
|
109
|
+
const inserted = await client.query(`INSERT INTO ${target} (hash, created_at) SELECT s.hash, s.created_at FROM ${source} s ` +
|
|
110
|
+
`WHERE NOT EXISTS (SELECT 1 FROM ${target} t WHERE t.hash = s.hash) ORDER BY s.created_at`);
|
|
111
|
+
const moved = inserted.rowCount ?? 0;
|
|
112
|
+
await client.query(`DROP TABLE ${source}`);
|
|
113
|
+
await client.query('SAVEPOINT drop_schema');
|
|
114
|
+
try {
|
|
115
|
+
await client.query(`DROP SCHEMA ${quoteIdentifier(options.from)}`);
|
|
116
|
+
await client.query('RELEASE SAVEPOINT drop_schema');
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// 2BP01: в схеме остались объекты — схема остаётся.
|
|
120
|
+
if (error.code !== '2BP01') {
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
await client.query('ROLLBACK TO SAVEPOINT drop_schema');
|
|
124
|
+
}
|
|
125
|
+
await client.query('COMMIT');
|
|
126
|
+
logger?.info({ from: options.from, to: options.to, moved }, 'moveJournal: готово');
|
|
127
|
+
return { moved };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
await client.query('ROLLBACK').catch((rollbackError) => {
|
|
131
|
+
failure = rollbackError instanceof Error ? rollbackError : new Error('ROLLBACK');
|
|
132
|
+
});
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
client.release(failure);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await pool.end();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type * as DrizzleNodePg from 'drizzle-orm/node-postgres';
|
|
2
|
+
import type * as DrizzleMigrator from 'drizzle-orm/node-postgres/migrator';
|
|
3
|
+
import type Pg from 'pg';
|
|
4
|
+
export type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
5
|
+
export type { Pool, PoolClient, PoolConfig } from 'pg';
|
|
6
|
+
/** Модуль `pg` (значение по умолчанию: `Pool`, `Client`, `escapeLiteral`). */
|
|
7
|
+
export type PgModule = typeof Pg;
|
|
8
|
+
/** Срок установки соединения CLI, миграций и `prepareDatabase`, мс (эталон O pool-options.ts). */
|
|
9
|
+
export declare const CONNECTION_TIMEOUT_MS = 5000;
|
|
10
|
+
/** Драйвер и migrator Drizzle для node-postgres. */
|
|
11
|
+
export interface DrizzleDriver {
|
|
12
|
+
/** `drizzle()` из `drizzle-orm/node-postgres`. */
|
|
13
|
+
readonly drizzle: typeof DrizzleNodePg.drizzle;
|
|
14
|
+
/** `migrate()` из `drizzle-orm/node-postgres/migrator`. */
|
|
15
|
+
readonly migrate: typeof DrizzleMigrator.migrate;
|
|
16
|
+
}
|
|
17
|
+
/** Модуль `pg`, загружается один раз при первом вызове. */
|
|
18
|
+
export declare function loadPg(): Promise<PgModule>;
|
|
19
|
+
/** Драйвер Drizzle (`drizzle`, `migrate`), загружается один раз при первом вызове. */
|
|
20
|
+
export declare function loadDrizzle(): Promise<DrizzleDriver>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { requirePeer } from '../internal/peer.js';
|
|
2
|
+
requirePeer('pg', 'database');
|
|
3
|
+
/** Срок установки соединения CLI, миграций и `prepareDatabase`, мс (эталон O pool-options.ts). */
|
|
4
|
+
export const CONNECTION_TIMEOUT_MS = 5000;
|
|
5
|
+
let pgPromise;
|
|
6
|
+
let drizzlePromise;
|
|
7
|
+
/** Модуль `pg`, загружается один раз при первом вызове. */
|
|
8
|
+
export function loadPg() {
|
|
9
|
+
pgPromise ??= import('pg').then((module) => module.default);
|
|
10
|
+
return pgPromise;
|
|
11
|
+
}
|
|
12
|
+
/** Драйвер Drizzle (`drizzle`, `migrate`), загружается один раз при первом вызове. */
|
|
13
|
+
export function loadDrizzle() {
|
|
14
|
+
drizzlePromise ??= Promise.all([
|
|
15
|
+
import('drizzle-orm/node-postgres'),
|
|
16
|
+
import('drizzle-orm/node-postgres/migrator'),
|
|
17
|
+
]).then(([driver, migrator]) => ({ drizzle: driver.drizzle, migrate: migrator.migrate }));
|
|
18
|
+
return drizzlePromise;
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Client } from 'pg';
|
|
2
|
+
import type { Logger } from 'pino';
|
|
3
|
+
/** Параметры `prepareDatabase` (prd.md §3.3). */
|
|
4
|
+
export interface KitPrepareDatabaseOptions {
|
|
5
|
+
/** Имя сервиса `^[a-z][a-z0-9-]{1,31}$`: роли `<svc>_owner`, `<svc>_app`, `<svc>_directory`, схема `<svc>`. */
|
|
6
|
+
readonly service: string;
|
|
7
|
+
/** Адрес БД сервиса под суперпользователем (или ролью с CREATEROLE). */
|
|
8
|
+
readonly adminUrl: string;
|
|
9
|
+
/** Пароль `<svc>_owner` (миграции). */
|
|
10
|
+
readonly ownerPassword: string;
|
|
11
|
+
/** Пароль `<svc>_app` (приложение). */
|
|
12
|
+
readonly appPassword: string;
|
|
13
|
+
}
|
|
14
|
+
/** Результат `prepareDatabase`. */
|
|
15
|
+
export interface KitPrepareDatabaseResult {
|
|
16
|
+
/** Имя подготовленной БД. */
|
|
17
|
+
readonly database: string;
|
|
18
|
+
/** Роли сервиса: owner, app, directory. */
|
|
19
|
+
readonly roles: readonly string[];
|
|
20
|
+
/** Роли, созданные этим вызовом (повтор — пусто). */
|
|
21
|
+
readonly created: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/** Имена ролей сервиса. */
|
|
24
|
+
export declare function serviceRoles(service: string): {
|
|
25
|
+
readonly owner: string;
|
|
26
|
+
readonly app: string;
|
|
27
|
+
readonly directory: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Права ролей сервиса на БД `database` (не на `current_database()`: вызывающий может быть подключён к `postgres`):
|
|
31
|
+
* владельцу — `CONNECT, CREATE` (схема журнала мигратора), приложению — только `CONNECT` (KT-19 п. 1).
|
|
32
|
+
*/
|
|
33
|
+
export declare function grantDatabaseAccess(client: Pick<Client, 'query'>, service: string, database: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Идемпотентно создаёт роли `<svc>_owner`, `<svc>_app` (LOGIN, NOBYPASSRLS), `<svc>_directory` (NOLOGIN, BYPASSRLS)
|
|
36
|
+
* и схему `<svc>` владельца; повтор меняет пароли на переданные. Права на таблицы и FORCE RLS — SQL-миграции сервиса.
|
|
37
|
+
* @throws {TypeError} имя сервиса вне формата (до подключения).
|
|
38
|
+
*/
|
|
39
|
+
export declare function prepareDatabase(options: KitPrepareDatabaseOptions, logger?: Logger): Promise<KitPrepareDatabaseResult>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { assertServiceName } from '../internal/service-name.js';
|
|
2
|
+
import { quoteIdentifier } from '../internal/sql-identifier.js';
|
|
3
|
+
import { CONNECTION_TIMEOUT_MS, loadPg } from './pg.js';
|
|
4
|
+
const LOGIN_ATTRIBUTES = 'LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS';
|
|
5
|
+
const DIRECTORY_ATTRIBUTES = 'NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE BYPASSRLS';
|
|
6
|
+
/** Имена ролей сервиса. */
|
|
7
|
+
export function serviceRoles(service) {
|
|
8
|
+
return { owner: `${service}_owner`, app: `${service}_app`, directory: `${service}_directory` };
|
|
9
|
+
}
|
|
10
|
+
/** CREATE или ALTER роли с атрибутами; `true` — роль создана сейчас. DDL ролей не принимает `$1`. */
|
|
11
|
+
async function upsertRole(client, role, attributes, logger) {
|
|
12
|
+
const { rowCount } = await client.query('SELECT 1 FROM pg_roles WHERE rolname = $1', [role]);
|
|
13
|
+
const exists = (rowCount ?? 0) > 0;
|
|
14
|
+
await client.query(`${exists ? 'ALTER' : 'CREATE'} ROLE ${quoteIdentifier(role)} ${attributes}`);
|
|
15
|
+
logger?.debug({ role, action: exists ? 'alter' : 'create' }, 'prepareDatabase: роль');
|
|
16
|
+
return !exists;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Права ролей сервиса на БД `database` (не на `current_database()`: вызывающий может быть подключён к `postgres`):
|
|
20
|
+
* владельцу — `CONNECT, CREATE` (схема журнала мигратора), приложению — только `CONNECT` (KT-19 п. 1).
|
|
21
|
+
*/
|
|
22
|
+
export async function grantDatabaseAccess(client, service, database) {
|
|
23
|
+
const roles = serviceRoles(service);
|
|
24
|
+
const db = quoteIdentifier(database);
|
|
25
|
+
await client.query(`GRANT CONNECT, CREATE ON DATABASE ${db} TO ${quoteIdentifier(roles.owner)}`);
|
|
26
|
+
await client.query(`GRANT CONNECT ON DATABASE ${db} TO ${quoteIdentifier(roles.app)}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Идемпотентно создаёт роли `<svc>_owner`, `<svc>_app` (LOGIN, NOBYPASSRLS), `<svc>_directory` (NOLOGIN, BYPASSRLS)
|
|
30
|
+
* и схему `<svc>` владельца; повтор меняет пароли на переданные. Права на таблицы и FORCE RLS — SQL-миграции сервиса.
|
|
31
|
+
* @throws {TypeError} имя сервиса вне формата (до подключения).
|
|
32
|
+
*/
|
|
33
|
+
export async function prepareDatabase(options, logger) {
|
|
34
|
+
assertServiceName(options.service, 'prepareDatabase');
|
|
35
|
+
const roles = serviceRoles(options.service);
|
|
36
|
+
const pg = await loadPg();
|
|
37
|
+
const client = new pg.Client({
|
|
38
|
+
connectionString: options.adminUrl,
|
|
39
|
+
connectionTimeoutMillis: CONNECTION_TIMEOUT_MS,
|
|
40
|
+
});
|
|
41
|
+
// Ошибка простаивающего клиента до end() не должна ронять процесс.
|
|
42
|
+
client.on('error', () => undefined);
|
|
43
|
+
await client.connect();
|
|
44
|
+
try {
|
|
45
|
+
const created = [];
|
|
46
|
+
const logins = [
|
|
47
|
+
[roles.owner, options.ownerPassword],
|
|
48
|
+
[roles.app, options.appPassword],
|
|
49
|
+
];
|
|
50
|
+
for (const [role, password] of logins) {
|
|
51
|
+
const attributes = `${LOGIN_ATTRIBUTES} PASSWORD ${pg.escapeLiteral(password)}`;
|
|
52
|
+
if (await upsertRole(client, role, attributes, logger)) {
|
|
53
|
+
created.push(role);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (await upsertRole(client, roles.directory, DIRECTORY_ATTRIBUTES, logger)) {
|
|
57
|
+
created.push(roles.directory);
|
|
58
|
+
}
|
|
59
|
+
// Владелец передаёт функции SECURITY DEFINER роли directory (ALTER FUNCTION … OWNER).
|
|
60
|
+
await client.query(`GRANT ${quoteIdentifier(roles.directory)} TO ${quoteIdentifier(roles.owner)}`);
|
|
61
|
+
const { rows } = await client.query('SELECT current_database() AS database');
|
|
62
|
+
const database = rows[0]?.database ?? '';
|
|
63
|
+
await grantDatabaseAccess(client, options.service, database);
|
|
64
|
+
const schema = quoteIdentifier(options.service);
|
|
65
|
+
const owner = quoteIdentifier(roles.owner);
|
|
66
|
+
await client.query(`CREATE SCHEMA IF NOT EXISTS ${schema} AUTHORIZATION ${owner}`);
|
|
67
|
+
await client.query(`ALTER SCHEMA ${schema} OWNER TO ${owner}`);
|
|
68
|
+
await client.query(`GRANT USAGE ON SCHEMA ${schema} TO ${quoteIdentifier(roles.app)}, ${quoteIdentifier(roles.directory)}`);
|
|
69
|
+
logger?.info({ service: options.service, database, created }, 'prepareDatabase: готово');
|
|
70
|
+
return { database, roles: [roles.owner, roles.app, roles.directory], created };
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await client.end();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Db } from './db.js';
|
|
2
|
+
import type { PoolClient } from './pg.js';
|
|
3
|
+
/**
|
|
4
|
+
* `fn` выполняется не больше чем в одной реплике одновременно: блокировка `pg_try_advisory_lock(hashtextextended(key,
|
|
5
|
+
* 0))` без ожидания; занято — `{ran: false}`. Блокировка держит одно соединение основного пула; `fn` получает его
|
|
6
|
+
* аргументом — работа на нём (например, `deleteInBatches({connection})`) не берёт второе соединение, поэтому
|
|
7
|
+
* достаточно `DB_POOL_MAX = 1` (KT-22 п. 4). Транзакции `fn` через `Db` берут другие соединения пула.
|
|
8
|
+
*
|
|
9
|
+
* Внутри `fn` на переданном соединении запрещены `SET` (без `LOCAL`) и `set_config(…, false)`: сеансовые настройки —
|
|
10
|
+
* тенант `app.tenant` в том числе — пережили бы `fn`. Перед возвратом в пул пакет выполняет `RESET ALL`. Настройки
|
|
11
|
+
* транзакции — `SET LOCAL` или `set_config(…, true)` внутри транзакции (`deleteInBatches` делает это сам).
|
|
12
|
+
* @throws {TypeError} пустой `key`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runExclusive<T>(db: Db, key: string, fn: (connection: PoolClient) => Promise<T>): Promise<{
|
|
15
|
+
readonly ran: true;
|
|
16
|
+
readonly result: T;
|
|
17
|
+
} | {
|
|
18
|
+
readonly ran: false;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// runExclusive (prd.md §3.3, AC-4.12, UC-9): pg_try_advisory_lock на выделенном соединении основного пула. Эталон O
|
|
2
|
+
// job-lock (Redis) не переносится — по PRD advisory lock. Review E4: RESET ALL перед возвратом соединения в пул.
|
|
3
|
+
import { Logger } from '@nestjs/common';
|
|
4
|
+
import { sanitizeError } from '../observability/sanitize-error.js';
|
|
5
|
+
import { mainPoolOf, withPoolClient } from './db.js';
|
|
6
|
+
const logger = new Logger('runExclusive');
|
|
7
|
+
/**
|
|
8
|
+
* `fn` выполняется не больше чем в одной реплике одновременно: блокировка `pg_try_advisory_lock(hashtextextended(key,
|
|
9
|
+
* 0))` без ожидания; занято — `{ran: false}`. Блокировка держит одно соединение основного пула; `fn` получает его
|
|
10
|
+
* аргументом — работа на нём (например, `deleteInBatches({connection})`) не берёт второе соединение, поэтому
|
|
11
|
+
* достаточно `DB_POOL_MAX = 1` (KT-22 п. 4). Транзакции `fn` через `Db` берут другие соединения пула.
|
|
12
|
+
*
|
|
13
|
+
* Внутри `fn` на переданном соединении запрещены `SET` (без `LOCAL`) и `set_config(…, false)`: сеансовые настройки —
|
|
14
|
+
* тенант `app.tenant` в том числе — пережили бы `fn`. Перед возвратом в пул пакет выполняет `RESET ALL`. Настройки
|
|
15
|
+
* транзакции — `SET LOCAL` или `set_config(…, true)` внутри транзакции (`deleteInBatches` делает это сам).
|
|
16
|
+
* @throws {TypeError} пустой `key`.
|
|
17
|
+
*/
|
|
18
|
+
export async function runExclusive(db, key, fn) {
|
|
19
|
+
if (key === '') {
|
|
20
|
+
throw new TypeError('runExclusive: пустой ключ');
|
|
21
|
+
}
|
|
22
|
+
return withPoolClient(mainPoolOf(db), async (client) => {
|
|
23
|
+
const { rows } = await client.query('select pg_try_advisory_lock(hashtextextended($1, 0)) as locked', [key]);
|
|
24
|
+
if (rows[0]?.locked !== true) {
|
|
25
|
+
logger.debug({ key }, 'runExclusive: занято');
|
|
26
|
+
return { ran: false };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return { ran: true, result: await fn(client) };
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
if (await unlock(client, key)) {
|
|
33
|
+
await resetSession(client, key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Снимает блокировку; `false` — клиент помечен сломанным (release(error) закроет его вместе с блокировкой). */
|
|
39
|
+
async function unlock(client, key) {
|
|
40
|
+
try {
|
|
41
|
+
await client.query('select pg_advisory_unlock(hashtextextended($1, 0))', [key]);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.warn({ key, err: sanitizeError(error) }, 'runExclusive: снять блокировку не удалось');
|
|
46
|
+
client.emit('error', error instanceof Error ? error : new Error('unlock'));
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Сеансовые настройки `fn` не уходят следующему запросу пула. `DISCARD ALL` не годится: снимает advisory-блокировки
|
|
52
|
+
* и подготовленные выражения драйвера. Сбой — клиент помечается сломанным и уничтожается пулом.
|
|
53
|
+
*/
|
|
54
|
+
async function resetSession(client, key) {
|
|
55
|
+
try {
|
|
56
|
+
await client.query('RESET ALL');
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.warn({ key, err: sanitizeError(error) }, 'runExclusive: сбросить сеанс не удалось');
|
|
60
|
+
client.emit('error', error instanceof Error ? error : new Error('reset'));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PgColumn, PgPolicy } from 'drizzle-orm/pg-core';
|
|
2
|
+
/** Настройка сеанса с тенантом транзакции (`set_config('app.tenant', …, true)`). */
|
|
3
|
+
export declare const TENANT_SETTING = "app.tenant";
|
|
4
|
+
/** Контекст операции с БД (prd.md §3.3): тенант, трассировка, субъект — поля `actor` конверта 01 §7.1. */
|
|
5
|
+
export interface DbContext {
|
|
6
|
+
/** Тенант (компания) или `null` — операция без тенанта (системная, `readGlobal`). */
|
|
7
|
+
readonly tenant: string | null;
|
|
8
|
+
/** Идентификатор трассировки запроса. */
|
|
9
|
+
readonly traceId: string;
|
|
10
|
+
/** Субъект операции. */
|
|
11
|
+
readonly actor: {
|
|
12
|
+
/** `client_id` служебного клиента. */
|
|
13
|
+
readonly clientId?: string;
|
|
14
|
+
/** Учётная запись пользователя. */
|
|
15
|
+
readonly accountId?: string;
|
|
16
|
+
/** Участник компании. */
|
|
17
|
+
readonly memberId?: string;
|
|
18
|
+
/** От чьего имени действует агент или служба. */
|
|
19
|
+
readonly onBehalfOf?: string;
|
|
20
|
+
/** Канал: `http`, `system`, `agent` и т. п. */
|
|
21
|
+
readonly channel: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Колонка тенанта `text not null` (по умолчанию имя `tenant`; проверки `assertStrictTenantIsolation` ищут `tenant`).
|
|
26
|
+
* Nullable-тенант — только у `createIdempotencyTable` и outbox E5.
|
|
27
|
+
*/
|
|
28
|
+
export declare function tenantColumn(name?: string): import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<string, [string, ...string[]]>>;
|
|
29
|
+
/**
|
|
30
|
+
* Строгая политика RLS `tenant_isolation`: `<column> = current_setting('app.tenant', true)` для чтения и записи.
|
|
31
|
+
* SQL генерирует `drizzle-kit` сервиса. В сервисе нужна SQL-миграция
|
|
32
|
+
* `ALTER TABLE <svc>.<таблица> FORCE ROW LEVEL SECURITY` и права `GRANT … TO <svc>_app` (README).
|
|
33
|
+
*/
|
|
34
|
+
export declare function tenantPolicy(column: PgColumn): PgPolicy;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// DbContext, tenantColumn, tenantPolicy (prd.md §3.3, AC-4.8, P0 — только строгий RLS). Эталон — O columns.ts
|
|
2
|
+
// tenantIsolation; I (пустой тенант видит всё) не переносится. Файл не импортирует pg: его грузит drizzle-kit со схемой.
|
|
3
|
+
import { sql } from 'drizzle-orm';
|
|
4
|
+
import { pgPolicy, text } from 'drizzle-orm/pg-core';
|
|
5
|
+
/** Настройка сеанса с тенантом транзакции (`set_config('app.tenant', …, true)`). */
|
|
6
|
+
export const TENANT_SETTING = 'app.tenant';
|
|
7
|
+
/**
|
|
8
|
+
* Колонка тенанта `text not null` (по умолчанию имя `tenant`; проверки `assertStrictTenantIsolation` ищут `tenant`).
|
|
9
|
+
* Nullable-тенант — только у `createIdempotencyTable` и outbox E5.
|
|
10
|
+
*/
|
|
11
|
+
export function tenantColumn(name = 'tenant') {
|
|
12
|
+
return text(name).notNull();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Строгая политика RLS `tenant_isolation`: `<column> = current_setting('app.tenant', true)` для чтения и записи.
|
|
16
|
+
* SQL генерирует `drizzle-kit` сервиса. В сервисе нужна SQL-миграция
|
|
17
|
+
* `ALTER TABLE <svc>.<таблица> FORCE ROW LEVEL SECURITY` и права `GRANT … TO <svc>_app` (README).
|
|
18
|
+
*/
|
|
19
|
+
export function tenantPolicy(column) {
|
|
20
|
+
const condition = sql `${column} = current_setting('app.tenant', true)`;
|
|
21
|
+
return pgPolicy('tenant_isolation', {
|
|
22
|
+
as: 'permissive',
|
|
23
|
+
for: 'all',
|
|
24
|
+
to: 'public',
|
|
25
|
+
using: condition,
|
|
26
|
+
withCheck: condition,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/** Ключ метаданных `@Api`. */
|
|
3
|
+
export declare const API_METADATA = "verdeect-kit:api";
|
|
4
|
+
/** Опции `@Api` (§3.5). */
|
|
5
|
+
export interface KitApiOptions {
|
|
6
|
+
/** Краткое описание операции. */
|
|
7
|
+
readonly summary: string;
|
|
8
|
+
/** Схема тела успешного ответа; нет — ответ без тела. */
|
|
9
|
+
readonly response?: z.ZodType;
|
|
10
|
+
/** Конверт ответа: `page` — `{items, nextCursor}`, `items` — `{items}`, `array` — массив; нет — объект как есть. */
|
|
11
|
+
readonly envelope?: 'page' | 'items' | 'array';
|
|
12
|
+
/** Коды ошибок маршрута из словаря `defineErrors` дополнительно к стандартным 400/401/403/422/429. */
|
|
13
|
+
readonly errors?: readonly string[];
|
|
14
|
+
/** Поля операции OpenAPI, которые генерация не выражает (SSE, сырой прокси); сливаются глубоко. */
|
|
15
|
+
readonly extra?: Readonly<Record<string, unknown>>;
|
|
16
|
+
}
|
|
17
|
+
/** Включает маршрут в документ OpenAPI `verdeect-kit openapi`; маршрут без `@Api` в документ не попадает. */
|
|
18
|
+
export declare function Api(options: KitApiOptions): MethodDecorator;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @Api — описание маршрута для OpenAPI 3.1 (prd.md §3.5, AC-3.7). Эталон — I open-api.decorator.ts (SetMetadata, envelope).
|
|
2
|
+
// По PRD: errors — коды словаря defineErrors (статусы берутся из реестра), extra — то, что генерация не выражает.
|
|
3
|
+
import { SetMetadata } from '@nestjs/common';
|
|
4
|
+
/** Ключ метаданных `@Api`. */
|
|
5
|
+
export const API_METADATA = 'verdeect-kit:api';
|
|
6
|
+
/** Включает маршрут в документ OpenAPI `verdeect-kit openapi`; маршрут без `@Api` в документ не попадает. */
|
|
7
|
+
export function Api(options) {
|
|
8
|
+
return SetMetadata(API_METADATA, options);
|
|
9
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DynamicModule, Type } from '@nestjs/common';
|
|
2
|
+
import type { Logger } from 'pino';
|
|
3
|
+
/** JSON Schema или объект OpenAPI. */
|
|
4
|
+
export type JsonObject = Record<string, unknown>;
|
|
5
|
+
/** Документ OpenAPI 3.1. */
|
|
6
|
+
export interface OpenApiDocument {
|
|
7
|
+
/** Версия спецификации. */
|
|
8
|
+
readonly openapi: '3.1.0';
|
|
9
|
+
/** Название и версия API. */
|
|
10
|
+
readonly info: {
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly version: string;
|
|
13
|
+
};
|
|
14
|
+
/** Операции по путям. */
|
|
15
|
+
readonly paths: Record<string, Record<string, JsonObject>>;
|
|
16
|
+
/** Схемы безопасности и общие схемы. */
|
|
17
|
+
readonly components: JsonObject;
|
|
18
|
+
}
|
|
19
|
+
/** Корень модуля в метаданных `imports`. */
|
|
20
|
+
type ModuleEntry = Type | DynamicModule | Promise<DynamicModule> | {
|
|
21
|
+
forwardRef: () => unknown;
|
|
22
|
+
};
|
|
23
|
+
/** Глубокое слияние `extra` (операция, `contracts/openapi.extra.json`): объекты сливаются, прочее заменяется. */
|
|
24
|
+
export declare function deepMerge(target: JsonObject, extra: Readonly<JsonObject>): JsonObject;
|
|
25
|
+
/** Схема успешного ответа, статус и описанные статусы ошибок маршрута для `expectMatchesApi`. Внутренняя функция. */
|
|
26
|
+
export declare function routeResponseSchema(route: readonly [Type, string]): {
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly schema: JsonObject | null;
|
|
29
|
+
readonly errorStatuses: readonly number[];
|
|
30
|
+
readonly errorSchema: JsonObject;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Документ OpenAPI 3.1 сервиса по метаданным модулей и контроллеров (§3.5): приложение не создаётся, окружение не
|
|
34
|
+
* нужно. В документ входят только маршруты с `@Api`; пути и коды отсортированы. Внутренняя функция (CLI, тесты).
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildOpenApiDocument(module: ModuleEntry, info: {
|
|
37
|
+
readonly title: string;
|
|
38
|
+
readonly version: string;
|
|
39
|
+
}, logger?: Pick<Logger, 'debug'>): Promise<OpenApiDocument>;
|
|
40
|
+
export {};
|