@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,122 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var KitHttpCoreModule_1;
|
|
11
|
+
// KitHttpCoreModule — внутренний глобальный модуль ядра createHttpApp (prd.md §3.2, §3.5; KT-11, KT-15 п. 2): логгер pino
|
|
12
|
+
// экземпляра, HealthRegistry, KitLifecycle, kitMetrics, KitExceptionFilter, nestjs-pino и GET /ok. Эталоны — O
|
|
13
|
+
// get-ok.controller.ts (/ok, no-store), регистрация фильтра APP_FILTER (O/I/P app.module.ts), O observability.module.ts
|
|
14
|
+
// (LoggerModule на опциях createLoggerOptions). По PRD: окружение — из ConfigService модуля сервиса (KitConfigModule),
|
|
15
|
+
// его нет — Error при сборке.
|
|
16
|
+
import { Controller, Get, Header, Module } from '@nestjs/common';
|
|
17
|
+
import { ConfigService } from '@nestjs/config';
|
|
18
|
+
import { APP_FILTER } from '@nestjs/core';
|
|
19
|
+
import { LoggerModule } from 'nestjs-pino';
|
|
20
|
+
import { pino } from 'pino';
|
|
21
|
+
import { KIT_LOGGER, KIT_METRICS } from '../internal/tokens.js';
|
|
22
|
+
import { HealthRegistry } from '../observability/health-registry.js';
|
|
23
|
+
import { KitLifecycle } from '../observability/lifecycle.js';
|
|
24
|
+
import { createLoggerOptions, } from '../observability/logger-options.js';
|
|
25
|
+
import { kitMetrics } from '../observability/metrics.js';
|
|
26
|
+
import { KitExceptionFilter } from './kit-exception.filter.js';
|
|
27
|
+
const CORE_ENV = Symbol('KIT_HTTP_CORE_ENV');
|
|
28
|
+
const LOGGER_PARAMS = Symbol('KIT_HTTP_LOGGER_PARAMS');
|
|
29
|
+
/** Проба живости процесса (эталон O get-ok.controller.ts); вне журнала доступа и OpenAPI. */
|
|
30
|
+
let KitOkController = class KitOkController {
|
|
31
|
+
/** `200 {status: 'ok'}`, `Cache-Control: no-store`. */
|
|
32
|
+
ok() {
|
|
33
|
+
return { status: 'ok' };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
__decorate([
|
|
37
|
+
Get('ok'),
|
|
38
|
+
Header('Cache-Control', 'no-store'),
|
|
39
|
+
__metadata("design:type", Function),
|
|
40
|
+
__metadata("design:paramtypes", []),
|
|
41
|
+
__metadata("design:returntype", Object)
|
|
42
|
+
], KitOkController.prototype, "ok", null);
|
|
43
|
+
KitOkController = __decorate([
|
|
44
|
+
Controller()
|
|
45
|
+
], KitOkController);
|
|
46
|
+
function coreEnv(config) {
|
|
47
|
+
if (config === null || config === undefined) {
|
|
48
|
+
throw new Error('createHttpApp: модуль сервиса не импортирует KitConfigModule.forRoot({schema})');
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
LOG_LEVEL: config.getOrThrow('LOG_LEVEL'),
|
|
52
|
+
REPLICA_ID: config.getOrThrow('REPLICA_ID'),
|
|
53
|
+
SHUTDOWN_TIMEOUT_MS: config.getOrThrow('SHUTDOWN_TIMEOUT_MS'),
|
|
54
|
+
SHUTDOWN_DRAIN_MS: config.getOrThrow('SHUTDOWN_DRAIN_MS'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Внутренний глобальный модуль ядра; подключает его `createHttpApp`. */
|
|
58
|
+
let KitHttpCoreModule = KitHttpCoreModule_1 = class KitHttpCoreModule {
|
|
59
|
+
/** Ядро одного экземпляра приложения; экспортирует KitLifecycle, HealthRegistry, KIT_METRICS, KIT_LOGGER. */
|
|
60
|
+
static forRoot(options) {
|
|
61
|
+
const loggerParams = {
|
|
62
|
+
provide: LOGGER_PARAMS,
|
|
63
|
+
useFactory: (env) => createLoggerOptions({
|
|
64
|
+
...options.logger,
|
|
65
|
+
service: options.service,
|
|
66
|
+
entry: 'http',
|
|
67
|
+
env,
|
|
68
|
+
redaction: options.redaction,
|
|
69
|
+
}),
|
|
70
|
+
inject: [CORE_ENV],
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
module: KitHttpCoreModule_1,
|
|
74
|
+
global: true,
|
|
75
|
+
imports: [
|
|
76
|
+
// Ограничение: nestjs-pino 5.3 держит один pino-http на процесс (rootLogger.js) — журнал доступа второго
|
|
77
|
+
// приложения в том же процессе (тесты) идёт с опциями первого; логгер ядра (KIT_LOGGER) — свой у каждого.
|
|
78
|
+
// README, «Один журнал доступа на процесс».
|
|
79
|
+
LoggerModule.forRootAsync({
|
|
80
|
+
useFactory: (params, logger) => ({
|
|
81
|
+
pinoHttp: [{ ...params.pinoHttp[0], logger }, params.pinoHttp[1]],
|
|
82
|
+
}),
|
|
83
|
+
inject: [LOGGER_PARAMS, KIT_LOGGER],
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
controllers: [KitOkController],
|
|
87
|
+
providers: [
|
|
88
|
+
{
|
|
89
|
+
provide: CORE_ENV,
|
|
90
|
+
useFactory: coreEnv,
|
|
91
|
+
inject: [{ token: ConfigService, optional: true }],
|
|
92
|
+
},
|
|
93
|
+
loggerParams,
|
|
94
|
+
{
|
|
95
|
+
provide: KIT_LOGGER,
|
|
96
|
+
useFactory: (params) => pino(...params.pinoHttp),
|
|
97
|
+
inject: [LOGGER_PARAMS],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
provide: HealthRegistry,
|
|
101
|
+
useFactory: (logger) => new HealthRegistry({ logger }),
|
|
102
|
+
inject: [KIT_LOGGER],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
provide: KitLifecycle,
|
|
106
|
+
useFactory: (logger, health, env) => new KitLifecycle({ logger, health, env }),
|
|
107
|
+
inject: [KIT_LOGGER, HealthRegistry, CORE_ENV],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
provide: KIT_METRICS,
|
|
111
|
+
useFactory: () => kitMetrics({ service: options.service }),
|
|
112
|
+
},
|
|
113
|
+
{ provide: APP_FILTER, useClass: KitExceptionFilter },
|
|
114
|
+
],
|
|
115
|
+
exports: [KitLifecycle, HealthRegistry, KIT_METRICS, KIT_LOGGER, LOGGER_PARAMS],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
KitHttpCoreModule = KitHttpCoreModule_1 = __decorate([
|
|
120
|
+
Module({})
|
|
121
|
+
], KitHttpCoreModule);
|
|
122
|
+
export { KitHttpCoreModule };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AnyColumn, type SQL } from 'drizzle-orm';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/** Значение ключа сортировки в курсоре. */
|
|
4
|
+
export type CursorValue = string | number | boolean | null;
|
|
5
|
+
/** Параметры страницы `?cursor=&limit=`; списки расширяют схему своими фильтрами (`.extend`). */
|
|
6
|
+
export declare const PageQuerySchema: z.ZodObject<{
|
|
7
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
8
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
/** Ответ списка: `nextCursor = null` — страниц больше нет. */
|
|
11
|
+
export interface Page<T> {
|
|
12
|
+
/** Строки страницы (не больше `limit`). */
|
|
13
|
+
readonly items: T[];
|
|
14
|
+
/** Курсор следующей страницы или `null`. */
|
|
15
|
+
readonly nextCursor: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** Курсор — base64url от JSON-массива ключей последней строки; не подписан. */
|
|
18
|
+
export declare function encodeCursor(keys: readonly CursorValue[]): string;
|
|
19
|
+
/** `undefined` → `null`; неразборчивый курсор или другое число ключей → 422 `VALIDATION_FAILED` `path: cursor`. */
|
|
20
|
+
export declare function decodeCursor(cursor: string | undefined, arity: number): CursorValue[] | null;
|
|
21
|
+
/** Репозиторий читает `limit + 1` строк: лишняя строка есть → курсор по последней из `limit`. */
|
|
22
|
+
export declare function pageOf<T>(rows: readonly T[], limit: number, keyOf: (row: T) => readonly CursorValue[]): Page<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Условие «после курсора» для `where`: `(c1, …, cn) > (v1, …, vn)` (`desc` — `<`) как ИЛИ-цепочка
|
|
25
|
+
* `c1 > v1 OR (c1 = v1 AND c2 > v2) OR …`. `cursor = null` → `undefined` (первая страница). Последний столбец обязан
|
|
26
|
+
* быть `primaryKey()` или `unique()` — иначе `Error` (ошибка разработчика). Длина курсора ≠ числу столбцов или `null`
|
|
27
|
+
* в ключе → 422 `VALIDATION_FAILED` `path: cursor`. Значения передаются параметрами без кодировщика столбца: строка
|
|
28
|
+
* ISO подходит и столбцу timestamp.
|
|
29
|
+
*/
|
|
30
|
+
export declare function afterCursor(cols: readonly AnyColumn[], cursor: readonly CursorValue[] | null, dir: 'asc' | 'desc'): SQL | undefined;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Keyset-пагинация (prd.md §3.5, AC-3.6). Эталон — I pagination.ts (encodeCursor, decodeCursor, pageOf, PageQuerySchema).
|
|
2
|
+
// По PRD: limit умолч. 100 (у I — 50), макс. 200; неверный курсор → 422 VALIDATION_FAILED path: cursor (у I — 400
|
|
3
|
+
// VALIDATION); ключи — JSON-скаляры, а не только строки; новое — afterCursor (условие Drizzle по ключам сортировки).
|
|
4
|
+
import { and, or, sql } from 'drizzle-orm';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { DomainError } from './errors.js';
|
|
7
|
+
/** Параметры страницы `?cursor=&limit=`; списки расширяют схему своими фильтрами (`.extend`). */
|
|
8
|
+
export const PageQuerySchema = z.object({
|
|
9
|
+
cursor: z.string().min(1).max(512).optional(),
|
|
10
|
+
limit: z.coerce.number().int().min(1).max(200).default(100),
|
|
11
|
+
});
|
|
12
|
+
function invalidCursor() {
|
|
13
|
+
return new DomainError('VALIDATION_FAILED', { issues: [{ path: 'cursor', code: 'invalid' }] });
|
|
14
|
+
}
|
|
15
|
+
function isCursorValue(value) {
|
|
16
|
+
return (value === null ||
|
|
17
|
+
typeof value === 'string' ||
|
|
18
|
+
typeof value === 'boolean' ||
|
|
19
|
+
(typeof value === 'number' && Number.isFinite(value)));
|
|
20
|
+
}
|
|
21
|
+
/** Курсор — base64url от JSON-массива ключей последней строки; не подписан. */
|
|
22
|
+
export function encodeCursor(keys) {
|
|
23
|
+
return Buffer.from(JSON.stringify(keys), 'utf8').toString('base64url');
|
|
24
|
+
}
|
|
25
|
+
/** `undefined` → `null`; неразборчивый курсор или другое число ключей → 422 `VALIDATION_FAILED` `path: cursor`. */
|
|
26
|
+
export function decodeCursor(cursor, arity) {
|
|
27
|
+
if (cursor === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
let value;
|
|
31
|
+
try {
|
|
32
|
+
value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw invalidCursor();
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(value) || value.length !== arity || !value.every(isCursorValue)) {
|
|
38
|
+
throw invalidCursor();
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
/** Репозиторий читает `limit + 1` строк: лишняя строка есть → курсор по последней из `limit`. */
|
|
43
|
+
export function pageOf(rows, limit, keyOf) {
|
|
44
|
+
if (rows.length <= limit) {
|
|
45
|
+
return { items: [...rows], nextCursor: null };
|
|
46
|
+
}
|
|
47
|
+
const items = rows.slice(0, limit);
|
|
48
|
+
const last = items[items.length - 1];
|
|
49
|
+
return { items, nextCursor: last === undefined ? null : encodeCursor(keyOf(last)) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Условие «после курсора» для `where`: `(c1, …, cn) > (v1, …, vn)` (`desc` — `<`) как ИЛИ-цепочка
|
|
53
|
+
* `c1 > v1 OR (c1 = v1 AND c2 > v2) OR …`. `cursor = null` → `undefined` (первая страница). Последний столбец обязан
|
|
54
|
+
* быть `primaryKey()` или `unique()` — иначе `Error` (ошибка разработчика). Длина курсора ≠ числу столбцов или `null`
|
|
55
|
+
* в ключе → 422 `VALIDATION_FAILED` `path: cursor`. Значения передаются параметрами без кодировщика столбца: строка
|
|
56
|
+
* ISO подходит и столбцу timestamp.
|
|
57
|
+
*/
|
|
58
|
+
export function afterCursor(cols, cursor, dir) {
|
|
59
|
+
const last = cols[cols.length - 1];
|
|
60
|
+
if (last === undefined || (!last.primary && !last.isUnique)) {
|
|
61
|
+
throw new Error('afterCursor: последний ключ сортировки должен быть уникальным');
|
|
62
|
+
}
|
|
63
|
+
if (cursor === null) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
// Ограничение: null в ключе не поддержан — ключи сортировки keyset должны быть NOT NULL (README, «Пагинация»).
|
|
67
|
+
if (cursor.length !== cols.length || cursor.includes(null)) {
|
|
68
|
+
throw invalidCursor();
|
|
69
|
+
}
|
|
70
|
+
const op = dir === 'asc' ? sql.raw('>') : sql.raw('<');
|
|
71
|
+
const branches = cols.map((col, index) => and(...cols.slice(0, index).map((prev, prevIndex) => sql `${prev} = ${cursor[prevIndex]}`), sql `${col} ${op} ${cursor[index]}`));
|
|
72
|
+
return or(...branches);
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PipeTransform } from '@nestjs/common';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Разбор тела, query или параметра запроса zod-схемой (§3.5). Успех — данные схемы (с умолчаниями); ошибка —
|
|
5
|
+
* `DomainError('VALIDATION_FAILED', {issues})`, путь — через точку, без пути — `(body)`. Схема публична: по ней
|
|
6
|
+
* построитель OpenAPI выводит схемы запроса.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ZodValidationPipe<T extends z.ZodType> implements PipeTransform<unknown, z.output<T>> {
|
|
9
|
+
readonly schema: T;
|
|
10
|
+
private readonly logger;
|
|
11
|
+
/** @param schema схема входа (читает построитель OpenAPI). */
|
|
12
|
+
constructor(schema: T);
|
|
13
|
+
/** Данные схемы или 422 `VALIDATION_FAILED`. */
|
|
14
|
+
transform(value: unknown): z.output<T>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var ZodValidationPipe_1;
|
|
11
|
+
// ZodValidationPipe (prd.md §3.5, AC-3.1): ошибка схемы → 422 VALIDATION_FAILED, details.issues [{path, code}] без значений.
|
|
12
|
+
// Эталон — O zod-validation.pipe.ts (P — идентичен) и O validation-failed.error.ts (форма issues). По PRD:
|
|
13
|
+
// DomainError('VALIDATION_FAILED') вместо ValidationFailedError; 400/413/415 даёт парсер тела Fastify, отображает фильтр.
|
|
14
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
15
|
+
import { DomainError } from './errors.js';
|
|
16
|
+
/**
|
|
17
|
+
* Разбор тела, query или параметра запроса zod-схемой (§3.5). Успех — данные схемы (с умолчаниями); ошибка —
|
|
18
|
+
* `DomainError('VALIDATION_FAILED', {issues})`, путь — через точку, без пути — `(body)`. Схема публична: по ней
|
|
19
|
+
* построитель OpenAPI выводит схемы запроса.
|
|
20
|
+
*/
|
|
21
|
+
let ZodValidationPipe = ZodValidationPipe_1 = class ZodValidationPipe {
|
|
22
|
+
schema;
|
|
23
|
+
logger = new Logger(ZodValidationPipe_1.name);
|
|
24
|
+
/** @param schema схема входа (читает построитель OpenAPI). */
|
|
25
|
+
constructor(schema) {
|
|
26
|
+
this.schema = schema;
|
|
27
|
+
}
|
|
28
|
+
/** Данные схемы или 422 `VALIDATION_FAILED`. */
|
|
29
|
+
transform(value) {
|
|
30
|
+
const result = this.schema.safeParse(value);
|
|
31
|
+
if (result.success) {
|
|
32
|
+
return result.data;
|
|
33
|
+
}
|
|
34
|
+
const issues = result.error.issues.map((issue) => ({
|
|
35
|
+
path: issue.path.map(String).join('.') || '(body)',
|
|
36
|
+
code: issue.code,
|
|
37
|
+
}));
|
|
38
|
+
// Только пути и коды: значения ввода в лог не попадают (эталон O).
|
|
39
|
+
this.logger.debug({ issues: issues.map((item) => `${item.path}:${item.code}`) }, 'запрос отклонён схемой');
|
|
40
|
+
throw new DomainError('VALIDATION_FAILED', { issues });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
ZodValidationPipe = ZodValidationPipe_1 = __decorate([
|
|
44
|
+
Injectable(),
|
|
45
|
+
__metadata("design:paramtypes", [Object])
|
|
46
|
+
], ZodValidationPipe);
|
|
47
|
+
export { ZodValidationPipe };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import type { Principal } from '@verdeect/auth';
|
|
4
|
+
import { type Observable } from 'rxjs';
|
|
5
|
+
import { Db } from '../database/db.js';
|
|
6
|
+
import { type KitIdempotentOptions } from '../internal/metadata.js';
|
|
7
|
+
import type { KitIdempotencyTable } from './table.js';
|
|
8
|
+
/** Токен таблицы идемпотентности сервиса (провайдер `KitIdempotencyModule`). */
|
|
9
|
+
export declare const KIT_IDEMPOTENCY_TABLE: unique symbol;
|
|
10
|
+
/** Печатные ASCII, ≤ 200 знаков (эталон O). */
|
|
11
|
+
export declare const KEY_FORMAT: RegExp;
|
|
12
|
+
/** Область ключа: тенант (или `null`) и субъект. */
|
|
13
|
+
export interface IdempotencyScope {
|
|
14
|
+
/** Тенант записи или `null` (служба без `tenantFromBody`). */
|
|
15
|
+
readonly tenant: string | null;
|
|
16
|
+
/** `user:<sub>`, `agent:<sub>`, `service:<client_id>`. */
|
|
17
|
+
readonly subject: string;
|
|
18
|
+
}
|
|
19
|
+
/** JSON с рекурсивно отсортированными ключами: одинаковое тело — одинаковый отпечаток (эталон O). */
|
|
20
|
+
export declare function canonicalJson(value: unknown): string;
|
|
21
|
+
/** SHA-256 hex метода, пути без query и канонического тела. */
|
|
22
|
+
export declare function fingerprintOf(method: string, path: string, body: unknown): string;
|
|
23
|
+
/**
|
|
24
|
+
* Область ключа (§3.3а): пользователь и агент — тенант токена (поле тела игнорируется); служба — `client_id` и тенант
|
|
25
|
+
* из поля `tenantFromBody`, если это непустая строка, иначе `null`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scopeOf(principal: Principal, body: unknown, options: KitIdempotentOptions): IdempotencyScope;
|
|
28
|
+
/**
|
|
29
|
+
* Интерсептор `@Idempotent` (§3.3а): регистрирует `KitIdempotencyModule` через `APP_INTERCEPTOR`. Повтор — сохранённый
|
|
30
|
+
* ответ и `Idempotent-Replayed: true`; другое тело — 422 `IDEMPOTENCY_KEY_REUSED`; запрос ещё идёт — 409
|
|
31
|
+
* `IDEMPOTENCY_IN_PROGRESS`. Сохраняются только успешные ответы.
|
|
32
|
+
*/
|
|
33
|
+
export declare class IdempotencyInterceptor implements NestInterceptor {
|
|
34
|
+
private readonly reflector;
|
|
35
|
+
private readonly db;
|
|
36
|
+
private readonly table;
|
|
37
|
+
private readonly logger;
|
|
38
|
+
constructor(reflector: Reflector, db: Db, table: KitIdempotencyTable);
|
|
39
|
+
/** Оборачивает обработчик маршрута с `@Idempotent`. */
|
|
40
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
41
|
+
private run;
|
|
42
|
+
private where;
|
|
43
|
+
private begin;
|
|
44
|
+
/**
|
|
45
|
+
* Обработчик упал: строка своей попытки удаляется — повтор выполнит запрос заново; перехваченную строку другой
|
|
46
|
+
* попытки не трогает; сбой release не подменяет ответ.
|
|
47
|
+
*/
|
|
48
|
+
private release;
|
|
49
|
+
/**
|
|
50
|
+
* Один повтор `complete` сразу после сбоя (эталон O). После двух сбоев ответ клиенту не меняется: строка остаётся
|
|
51
|
+
* `in_progress` и через 5 минут переиспользуется. Ответ пишется только своей попытки: строку перехватил повторный
|
|
52
|
+
* запрос (обработчик дольше 5 минут) — ответ не сохраняется, `warn` без ключа и тела (KT-22 п. 3).
|
|
53
|
+
*/
|
|
54
|
+
private completeWithRetry;
|
|
55
|
+
}
|