@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,203 @@
|
|
|
1
|
+
import { type PgSchema } from 'drizzle-orm/pg-core';
|
|
2
|
+
/** Имя таблицы идемпотентности в схеме сервиса. */
|
|
3
|
+
export declare const IDEMPOTENCY_TABLE = "idempotency_key";
|
|
4
|
+
/** Имя функции очистки `SECURITY DEFINER` в схеме сервиса. */
|
|
5
|
+
export declare const PURGE_FUNCTION = "idempotency_purge";
|
|
6
|
+
/** Уникальное ограничение области и ключа (`ON CONFLICT ON CONSTRAINT`). */
|
|
7
|
+
export declare const SCOPE_CONSTRAINT = "idempotency_key_scope_key";
|
|
8
|
+
declare function defineTable(schema: PgSchema): import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
9
|
+
name: "idempotency_key";
|
|
10
|
+
schema: string;
|
|
11
|
+
columns: {
|
|
12
|
+
tenant: import("drizzle-orm/pg-core").PgColumn<{
|
|
13
|
+
name: "tenant";
|
|
14
|
+
tableName: "idempotency_key";
|
|
15
|
+
dataType: "string";
|
|
16
|
+
columnType: "PgText";
|
|
17
|
+
data: string;
|
|
18
|
+
driverParam: string;
|
|
19
|
+
notNull: false;
|
|
20
|
+
hasDefault: false;
|
|
21
|
+
isPrimaryKey: false;
|
|
22
|
+
isAutoincrement: false;
|
|
23
|
+
hasRuntimeDefault: false;
|
|
24
|
+
enumValues: [string, ...string[]];
|
|
25
|
+
baseColumn: never;
|
|
26
|
+
identity: undefined;
|
|
27
|
+
generated: undefined;
|
|
28
|
+
}, {}, {}>;
|
|
29
|
+
subject: import("drizzle-orm/pg-core").PgColumn<{
|
|
30
|
+
name: "subject";
|
|
31
|
+
tableName: "idempotency_key";
|
|
32
|
+
dataType: "string";
|
|
33
|
+
columnType: "PgText";
|
|
34
|
+
data: string;
|
|
35
|
+
driverParam: string;
|
|
36
|
+
notNull: true;
|
|
37
|
+
hasDefault: false;
|
|
38
|
+
isPrimaryKey: false;
|
|
39
|
+
isAutoincrement: false;
|
|
40
|
+
hasRuntimeDefault: false;
|
|
41
|
+
enumValues: [string, ...string[]];
|
|
42
|
+
baseColumn: never;
|
|
43
|
+
identity: undefined;
|
|
44
|
+
generated: undefined;
|
|
45
|
+
}, {}, {}>;
|
|
46
|
+
key: import("drizzle-orm/pg-core").PgColumn<{
|
|
47
|
+
name: "key";
|
|
48
|
+
tableName: "idempotency_key";
|
|
49
|
+
dataType: "string";
|
|
50
|
+
columnType: "PgText";
|
|
51
|
+
data: string;
|
|
52
|
+
driverParam: string;
|
|
53
|
+
notNull: true;
|
|
54
|
+
hasDefault: false;
|
|
55
|
+
isPrimaryKey: false;
|
|
56
|
+
isAutoincrement: false;
|
|
57
|
+
hasRuntimeDefault: false;
|
|
58
|
+
enumValues: [string, ...string[]];
|
|
59
|
+
baseColumn: never;
|
|
60
|
+
identity: undefined;
|
|
61
|
+
generated: undefined;
|
|
62
|
+
}, {}, {}>;
|
|
63
|
+
fingerprint: import("drizzle-orm/pg-core").PgColumn<{
|
|
64
|
+
name: "fingerprint";
|
|
65
|
+
tableName: "idempotency_key";
|
|
66
|
+
dataType: "string";
|
|
67
|
+
columnType: "PgText";
|
|
68
|
+
data: string;
|
|
69
|
+
driverParam: string;
|
|
70
|
+
notNull: true;
|
|
71
|
+
hasDefault: false;
|
|
72
|
+
isPrimaryKey: false;
|
|
73
|
+
isAutoincrement: false;
|
|
74
|
+
hasRuntimeDefault: false;
|
|
75
|
+
enumValues: [string, ...string[]];
|
|
76
|
+
baseColumn: never;
|
|
77
|
+
identity: undefined;
|
|
78
|
+
generated: undefined;
|
|
79
|
+
}, {}, {}>;
|
|
80
|
+
attempt: import("drizzle-orm/pg-core").PgColumn<{
|
|
81
|
+
name: "attempt";
|
|
82
|
+
tableName: "idempotency_key";
|
|
83
|
+
dataType: "string";
|
|
84
|
+
columnType: "PgUUID";
|
|
85
|
+
data: string;
|
|
86
|
+
driverParam: string;
|
|
87
|
+
notNull: true;
|
|
88
|
+
hasDefault: true;
|
|
89
|
+
isPrimaryKey: false;
|
|
90
|
+
isAutoincrement: false;
|
|
91
|
+
hasRuntimeDefault: false;
|
|
92
|
+
enumValues: undefined;
|
|
93
|
+
baseColumn: never;
|
|
94
|
+
identity: undefined;
|
|
95
|
+
generated: undefined;
|
|
96
|
+
}, {}, {}>;
|
|
97
|
+
state: import("drizzle-orm/pg-core").PgColumn<{
|
|
98
|
+
name: "state";
|
|
99
|
+
tableName: "idempotency_key";
|
|
100
|
+
dataType: "string";
|
|
101
|
+
columnType: "PgText";
|
|
102
|
+
data: string;
|
|
103
|
+
driverParam: string;
|
|
104
|
+
notNull: true;
|
|
105
|
+
hasDefault: true;
|
|
106
|
+
isPrimaryKey: false;
|
|
107
|
+
isAutoincrement: false;
|
|
108
|
+
hasRuntimeDefault: false;
|
|
109
|
+
enumValues: [string, ...string[]];
|
|
110
|
+
baseColumn: never;
|
|
111
|
+
identity: undefined;
|
|
112
|
+
generated: undefined;
|
|
113
|
+
}, {}, {}>;
|
|
114
|
+
responseStatus: import("drizzle-orm/pg-core").PgColumn<{
|
|
115
|
+
name: "response_status";
|
|
116
|
+
tableName: "idempotency_key";
|
|
117
|
+
dataType: "number";
|
|
118
|
+
columnType: "PgSmallInt";
|
|
119
|
+
data: number;
|
|
120
|
+
driverParam: string | number;
|
|
121
|
+
notNull: false;
|
|
122
|
+
hasDefault: false;
|
|
123
|
+
isPrimaryKey: false;
|
|
124
|
+
isAutoincrement: false;
|
|
125
|
+
hasRuntimeDefault: false;
|
|
126
|
+
enumValues: undefined;
|
|
127
|
+
baseColumn: never;
|
|
128
|
+
identity: undefined;
|
|
129
|
+
generated: undefined;
|
|
130
|
+
}, {}, {}>;
|
|
131
|
+
responseBody: import("drizzle-orm/pg-core").PgColumn<{
|
|
132
|
+
name: "response_body";
|
|
133
|
+
tableName: "idempotency_key";
|
|
134
|
+
dataType: "json";
|
|
135
|
+
columnType: "PgJsonb";
|
|
136
|
+
data: unknown;
|
|
137
|
+
driverParam: unknown;
|
|
138
|
+
notNull: false;
|
|
139
|
+
hasDefault: false;
|
|
140
|
+
isPrimaryKey: false;
|
|
141
|
+
isAutoincrement: false;
|
|
142
|
+
hasRuntimeDefault: false;
|
|
143
|
+
enumValues: undefined;
|
|
144
|
+
baseColumn: never;
|
|
145
|
+
identity: undefined;
|
|
146
|
+
generated: undefined;
|
|
147
|
+
}, {}, {}>;
|
|
148
|
+
responseLocation: import("drizzle-orm/pg-core").PgColumn<{
|
|
149
|
+
name: "response_location";
|
|
150
|
+
tableName: "idempotency_key";
|
|
151
|
+
dataType: "string";
|
|
152
|
+
columnType: "PgText";
|
|
153
|
+
data: string;
|
|
154
|
+
driverParam: string;
|
|
155
|
+
notNull: false;
|
|
156
|
+
hasDefault: false;
|
|
157
|
+
isPrimaryKey: false;
|
|
158
|
+
isAutoincrement: false;
|
|
159
|
+
hasRuntimeDefault: false;
|
|
160
|
+
enumValues: [string, ...string[]];
|
|
161
|
+
baseColumn: never;
|
|
162
|
+
identity: undefined;
|
|
163
|
+
generated: undefined;
|
|
164
|
+
}, {}, {}>;
|
|
165
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
166
|
+
name: "created_at";
|
|
167
|
+
tableName: "idempotency_key";
|
|
168
|
+
dataType: "date";
|
|
169
|
+
columnType: "PgTimestamp";
|
|
170
|
+
data: Date;
|
|
171
|
+
driverParam: string;
|
|
172
|
+
notNull: true;
|
|
173
|
+
hasDefault: true;
|
|
174
|
+
isPrimaryKey: false;
|
|
175
|
+
isAutoincrement: false;
|
|
176
|
+
hasRuntimeDefault: false;
|
|
177
|
+
enumValues: undefined;
|
|
178
|
+
baseColumn: never;
|
|
179
|
+
identity: undefined;
|
|
180
|
+
generated: undefined;
|
|
181
|
+
}, {}, {}>;
|
|
182
|
+
};
|
|
183
|
+
dialect: "pg";
|
|
184
|
+
}>;
|
|
185
|
+
/** Таблица идемпотентности сервиса (`createIdempotencyTable`). */
|
|
186
|
+
export type KitIdempotencyTable = ReturnType<typeof defineTable>;
|
|
187
|
+
/**
|
|
188
|
+
* Таблица `idempotency_key` в схеме сервиса (§6): область (`tenant?`, `subject`), ключ, отпечаток запроса, метка
|
|
189
|
+
* попытки `attempt` (ответ пишет только захватившая строку попытка), состояние,
|
|
190
|
+
* сохранённый ответ (статус, тело, `Location`), время создания; RLS
|
|
191
|
+
* `tenant IS NOT DISTINCT FROM nullif(current_setting('app.tenant', true), '')`. SQL генерирует `drizzle-kit` сервиса;
|
|
192
|
+
* `FORCE ROW LEVEL SECURITY`, права `<svc>_app` и функцию очистки (`idempotencyPurgeSql`) — SQL-миграция сервиса.
|
|
193
|
+
*/
|
|
194
|
+
export declare function createIdempotencyTable(schema: PgSchema): KitIdempotencyTable;
|
|
195
|
+
/**
|
|
196
|
+
* Текст SQL функции очистки `<схема>.idempotency_purge(batch integer)` для SQL-миграции сервиса (`drizzle-kit generate
|
|
197
|
+
* --custom`): `SECURITY DEFINER`, владелец `<схема>_directory`, `search_path`, `REVOKE ALL … FROM PUBLIC`,
|
|
198
|
+
* `GRANT EXECUTE … TO <схема>_app`; удаляет записи старше 24 ч пачкой. Без функции `KitIdempotencyModule` отказывает
|
|
199
|
+
* при старте. Операторы разделены `--> statement-breakpoint`.
|
|
200
|
+
* @throws {TypeError} имя схемы вне формата имени сервиса `^[a-z][a-z0-9-]{1,31}$`.
|
|
201
|
+
*/
|
|
202
|
+
export declare function idempotencyPurgeSql(schema: PgSchema): string;
|
|
203
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// createIdempotencyTable, idempotencyPurgeSql (prd.md §3.3а v1.14, §6, AC-4.8, AC-4.14). Эталон — O idempotency.schema.ts,
|
|
2
|
+
// 0002_functions.sql idempotency_purge. По PRD: tenant nullable, subject вместо actor, ответ с Location, политика
|
|
3
|
+
// IS NOT DISTINCT FROM nullif(…). Файл без pg: его грузит drizzle-kit со схемой сервиса.
|
|
4
|
+
import { sql } from 'drizzle-orm';
|
|
5
|
+
import { check, index, jsonb, pgPolicy, smallint, text, timestamp, unique, uuid, } from 'drizzle-orm/pg-core';
|
|
6
|
+
import { assertServiceName } from '../internal/service-name.js';
|
|
7
|
+
import { quoteIdentifier } from '../internal/sql-identifier.js';
|
|
8
|
+
/** Имя таблицы идемпотентности в схеме сервиса. */
|
|
9
|
+
export const IDEMPOTENCY_TABLE = 'idempotency_key';
|
|
10
|
+
/** Имя функции очистки `SECURITY DEFINER` в схеме сервиса. */
|
|
11
|
+
export const PURGE_FUNCTION = 'idempotency_purge';
|
|
12
|
+
/** Уникальное ограничение области и ключа (`ON CONFLICT ON CONSTRAINT`). */
|
|
13
|
+
export const SCOPE_CONSTRAINT = 'idempotency_key_scope_key';
|
|
14
|
+
/** Разделитель операторов миграций Drizzle. */
|
|
15
|
+
const BREAKPOINT = '\n--> statement-breakpoint\n';
|
|
16
|
+
function defineTable(schema) {
|
|
17
|
+
return schema.table(IDEMPOTENCY_TABLE, {
|
|
18
|
+
tenant: text('tenant'),
|
|
19
|
+
subject: text('subject').notNull(),
|
|
20
|
+
key: text('key').notNull(),
|
|
21
|
+
fingerprint: text('fingerprint').notNull(),
|
|
22
|
+
// Метка попытки, захватившей строку: ответ пишет только она (KT-22 п. 3).
|
|
23
|
+
attempt: uuid('attempt').notNull().defaultRandom(),
|
|
24
|
+
state: text('state').notNull().default('in_progress'),
|
|
25
|
+
responseStatus: smallint('response_status'),
|
|
26
|
+
responseBody: jsonb('response_body'),
|
|
27
|
+
responseLocation: text('response_location'),
|
|
28
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
29
|
+
}, (t) => [
|
|
30
|
+
// PK по nullable-колонке невозможен: область и ключ — уникальное ограничение NULLS NOT DISTINCT (PG ≥ 15).
|
|
31
|
+
unique(SCOPE_CONSTRAINT).on(t.tenant, t.subject, t.key).nullsNotDistinct(),
|
|
32
|
+
index('idempotency_key_created_idx').on(t.createdAt),
|
|
33
|
+
check('idempotency_key_state_check', sql `${t.state} IN ('in_progress', 'done')`),
|
|
34
|
+
pgPolicy('idempotency_key_scope', {
|
|
35
|
+
as: 'permissive',
|
|
36
|
+
for: 'all',
|
|
37
|
+
to: 'public',
|
|
38
|
+
using: sql `${t.tenant} IS NOT DISTINCT FROM nullif(current_setting('app.tenant', true), '')`,
|
|
39
|
+
withCheck: sql `${t.tenant} IS NOT DISTINCT FROM nullif(current_setting('app.tenant', true), '')`,
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Таблица `idempotency_key` в схеме сервиса (§6): область (`tenant?`, `subject`), ключ, отпечаток запроса, метка
|
|
45
|
+
* попытки `attempt` (ответ пишет только захватившая строку попытка), состояние,
|
|
46
|
+
* сохранённый ответ (статус, тело, `Location`), время создания; RLS
|
|
47
|
+
* `tenant IS NOT DISTINCT FROM nullif(current_setting('app.tenant', true), '')`. SQL генерирует `drizzle-kit` сервиса;
|
|
48
|
+
* `FORCE ROW LEVEL SECURITY`, права `<svc>_app` и функцию очистки (`idempotencyPurgeSql`) — SQL-миграция сервиса.
|
|
49
|
+
*/
|
|
50
|
+
export function createIdempotencyTable(schema) {
|
|
51
|
+
return defineTable(schema);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Текст SQL функции очистки `<схема>.idempotency_purge(batch integer)` для SQL-миграции сервиса (`drizzle-kit generate
|
|
55
|
+
* --custom`): `SECURITY DEFINER`, владелец `<схема>_directory`, `search_path`, `REVOKE ALL … FROM PUBLIC`,
|
|
56
|
+
* `GRANT EXECUTE … TO <схема>_app`; удаляет записи старше 24 ч пачкой. Без функции `KitIdempotencyModule` отказывает
|
|
57
|
+
* при старте. Операторы разделены `--> statement-breakpoint`.
|
|
58
|
+
* @throws {TypeError} имя схемы вне формата имени сервиса `^[a-z][a-z0-9-]{1,31}$`.
|
|
59
|
+
*/
|
|
60
|
+
export function idempotencyPurgeSql(schema) {
|
|
61
|
+
const name = schema.schemaName;
|
|
62
|
+
assertServiceName(name, 'idempotencyPurgeSql');
|
|
63
|
+
const s = quoteIdentifier(name);
|
|
64
|
+
const table = `${s}.${quoteIdentifier(IDEMPOTENCY_TABLE)}`;
|
|
65
|
+
const fn = `${s}.${quoteIdentifier(PURGE_FUNCTION)}`;
|
|
66
|
+
const directory = quoteIdentifier(`${name}_directory`);
|
|
67
|
+
const app = quoteIdentifier(`${name}_app`);
|
|
68
|
+
return [
|
|
69
|
+
`CREATE OR REPLACE FUNCTION ${fn}(batch integer) RETURNS integer
|
|
70
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = ${s}, pg_temp AS $$
|
|
71
|
+
DECLARE deleted integer;
|
|
72
|
+
BEGIN
|
|
73
|
+
DELETE FROM ${table} i WHERE i.ctid IN (
|
|
74
|
+
SELECT o.ctid FROM ${table} o
|
|
75
|
+
WHERE o.created_at < now() - interval '24 hours' ORDER BY o.created_at LIMIT batch);
|
|
76
|
+
GET DIAGNOSTICS deleted = ROW_COUNT;
|
|
77
|
+
RETURN deleted;
|
|
78
|
+
END $$;`,
|
|
79
|
+
`GRANT SELECT, DELETE ON ${table} TO ${directory};`,
|
|
80
|
+
`GRANT CREATE ON SCHEMA ${s} TO ${directory};`,
|
|
81
|
+
`ALTER FUNCTION ${fn}(integer) OWNER TO ${directory};`,
|
|
82
|
+
`REVOKE CREATE ON SCHEMA ${s} FROM ${directory};`,
|
|
83
|
+
`REVOKE ALL ON FUNCTION ${fn}(integer) FROM PUBLIC;`,
|
|
84
|
+
`GRANT EXECUTE ON FUNCTION ${fn}(integer) TO ${app};`,
|
|
85
|
+
].join(BREAKPOINT);
|
|
86
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Итог прохода — метка `result` метрики `verdeect_cleanup_runs_total` (без `error`). */
|
|
2
|
+
export type CleanupResult = 'ok' | 'partial' | 'interrupted';
|
|
3
|
+
/**
|
|
4
|
+
* `ok` — удалять больше нечего; `partial` — упёрся в `maxBatches`, записи старше срока остались; `interrupted` —
|
|
5
|
+
* остановлен (выкат), алерт `VerdeectCleanupFailing` его не учитывает.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resultOf(complete: boolean, batches: number, maxBatches: number): CleanupResult;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Итог прохода очистки (prd.md §3.3а, §3.4, AC-9.5; KT-22 п. 5): общий для очистки идемпотентности и сообщений —
|
|
2
|
+
// messaging не импортирует idempotency (ARCHITECTURE). Перенос resultOf из kit-idempotency.module.ts (E4).
|
|
3
|
+
/**
|
|
4
|
+
* `ok` — удалять больше нечего; `partial` — упёрся в `maxBatches`, записи старше срока остались; `interrupted` —
|
|
5
|
+
* остановлен (выкат), алерт `VerdeectCleanupFailing` его не учитывает.
|
|
6
|
+
*/
|
|
7
|
+
export function resultOf(complete, batches, maxBatches) {
|
|
8
|
+
if (complete) {
|
|
9
|
+
return 'ok';
|
|
10
|
+
}
|
|
11
|
+
return batches === maxBatches ? 'partial' : 'interrupted';
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Формат префикса идентификатора `newId`: 2–5 строчных латинских букв (реестр префиксов — README). */
|
|
2
|
+
export declare const ID_PREFIX: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* ULID: 10 знаков времени (`now`, мс, по умолчанию `Date.now()`) и 16 случайных знаков Crockford base32.
|
|
5
|
+
* Сортировка строк совпадает с порядком по времени до миллисекунды.
|
|
6
|
+
* @throws {RangeError} `now` не целое, отрицательное или больше 2^48 − 1.
|
|
7
|
+
*/
|
|
8
|
+
export declare function ulid(now?: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* Идентификатор сущности `<prefix>_<ULID>`, например `fil_01J…` (AC-4.9).
|
|
11
|
+
* @throws {TypeError} префикс вне формата `^[a-z]{2,5}$`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function newId(prefix: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ulid, newId (prd.md §3.3, AC-4.9). Эталон — O ids.ts: алфавит Crockford, 10 знаков времени, 16 случайных;
|
|
2
|
+
// по PRD — проверка префикса ^[a-z]{2,5}$, без монотонности внутри миллисекунды.
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
/** Алфавит Crockford base32 (без I, L, O, U). */
|
|
5
|
+
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
6
|
+
/** Наибольшее время ULID — 48 бит миллисекунд. */
|
|
7
|
+
const MAX_TIME = 2 ** 48 - 1;
|
|
8
|
+
/** Формат префикса идентификатора `newId`: 2–5 строчных латинских букв (реестр префиксов — README). */
|
|
9
|
+
export const ID_PREFIX = /^[a-z]{2,5}$/;
|
|
10
|
+
/**
|
|
11
|
+
* ULID: 10 знаков времени (`now`, мс, по умолчанию `Date.now()`) и 16 случайных знаков Crockford base32.
|
|
12
|
+
* Сортировка строк совпадает с порядком по времени до миллисекунды.
|
|
13
|
+
* @throws {RangeError} `now` не целое, отрицательное или больше 2^48 − 1.
|
|
14
|
+
*/
|
|
15
|
+
export function ulid(now = Date.now()) {
|
|
16
|
+
if (!Number.isInteger(now) || now < 0 || now > MAX_TIME) {
|
|
17
|
+
throw new RangeError('ulid: время вне диапазона');
|
|
18
|
+
}
|
|
19
|
+
let time = '';
|
|
20
|
+
let rest = now;
|
|
21
|
+
for (let index = 0; index < 10; index += 1) {
|
|
22
|
+
time = CROCKFORD.charAt(rest % 32) + time;
|
|
23
|
+
rest = Math.floor(rest / 32);
|
|
24
|
+
}
|
|
25
|
+
let random = '';
|
|
26
|
+
// 256 делится на 32 без остатка — взятие по модулю не смещает распределение (O).
|
|
27
|
+
for (const byte of randomBytes(16)) {
|
|
28
|
+
random += CROCKFORD.charAt(byte % 32);
|
|
29
|
+
}
|
|
30
|
+
return time + random;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Идентификатор сущности `<prefix>_<ULID>`, например `fil_01J…` (AC-4.9).
|
|
34
|
+
* @throws {TypeError} префикс вне формата `^[a-z]{2,5}$`.
|
|
35
|
+
*/
|
|
36
|
+
export function newId(prefix) {
|
|
37
|
+
if (!ID_PREFIX.test(prefix)) {
|
|
38
|
+
throw new TypeError(`newId: префикс «${prefix}» вне формата ^[a-z]{2,5}$`);
|
|
39
|
+
}
|
|
40
|
+
return `${prefix}_${ulid()}`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Ключ метаданных `@Idempotent` на методе обработчика; значение — `KitIdempotentOptions`. */
|
|
2
|
+
export declare const IDEMPOTENT_METADATA = "verdeect-kit:idempotent";
|
|
3
|
+
/** Опции `@Idempotent` (prd.md §3.3а). */
|
|
4
|
+
export interface KitIdempotentOptions {
|
|
5
|
+
/** Ключ `Idempotency-Key` обязателен: без него — 400 `IDEMPOTENCY_KEY_REQUIRED`. По умолчанию `false`. */
|
|
6
|
+
readonly required?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Путь поля тела через точку, из которого берётся тенант области для служебного принципала
|
|
9
|
+
* (`service`); у пользователя и агента тенант — только из принципала, поле игнорируется.
|
|
10
|
+
*/
|
|
11
|
+
readonly tenantFromBody?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Ключи метаданных декораторов: Idempotent → @Api без цикла http ↔ idempotency (ARCHITECTURE.md; prd.md §3.3а, §3.5).
|
|
2
|
+
/** Ключ метаданных `@Idempotent` на методе обработчика; значение — `KitIdempotentOptions`. */
|
|
3
|
+
export const IDEMPOTENT_METADATA = 'verdeect-kit:idempotent';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Таблица outbox в схеме сервиса. */
|
|
2
|
+
export declare const OUTBOX_TABLE = "outbox";
|
|
3
|
+
/** Таблица inbox в схеме сервиса. */
|
|
4
|
+
export declare const INBOX_TABLE = "inbox";
|
|
5
|
+
/** Политика outbox. */
|
|
6
|
+
export declare const OUTBOX_POLICY = "outbox_tenant_isolation";
|
|
7
|
+
/** Исключение строгой модели (§3.4, §6): пустой тенант — все строки (relay, очистка); иначе — только строки тенанта. */
|
|
8
|
+
export declare const OUTBOX_POLICY_SQL = "coalesce(current_setting('app.tenant', true), '') = '' OR tenant = current_setting('app.tenant', true)";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Имена таблиц сообщений и политика outbox (prd.md §3.4, §6, AC-4.8): общие для /messaging и /testing
|
|
2
|
+
// (assertStrictTenantIsolation, expectOutbox) — /testing не грузит /messaging (без amqplib).
|
|
3
|
+
/** Таблица outbox в схеме сервиса. */
|
|
4
|
+
export const OUTBOX_TABLE = 'outbox';
|
|
5
|
+
/** Таблица inbox в схеме сервиса. */
|
|
6
|
+
export const INBOX_TABLE = 'inbox';
|
|
7
|
+
/** Политика outbox. */
|
|
8
|
+
export const OUTBOX_POLICY = 'outbox_tenant_isolation';
|
|
9
|
+
/** Исключение строгой модели (§3.4, §6): пустой тенант — все строки (relay, очистка); иначе — только строки тенанта. */
|
|
10
|
+
export const OUTBOX_POLICY_SQL = "coalesce(current_setting('app.tenant', true), '') = '' OR tenant = current_setting('app.tenant', true)";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type Importer = (specifier: string) => Promise<unknown>;
|
|
2
|
+
/** Текст ошибки AC-1.2. */
|
|
3
|
+
export declare function missingPeerMessage(library: string, subpath: string): string;
|
|
4
|
+
/** Загружает peer через importer; отсутствие самого пакета → ошибка AC-1.2, прочее — как есть. */
|
|
5
|
+
export declare function loadPeerWith<T>(importer: Importer, library: string, subpath: string): Promise<T>;
|
|
6
|
+
/** Опциональный peer подпути; вызывается top-level await в модуле подпути. */
|
|
7
|
+
export declare function loadPeer<T>(library: string, subpath: string): Promise<T>;
|
|
8
|
+
type Resolver = (specifier: string) => string;
|
|
9
|
+
/** Синхронная проверка peer через resolver; отсутствие самого пакета → ошибка AC-1.2, прочее — как есть. */
|
|
10
|
+
export declare function requirePeerWith(resolver: Resolver, library: string, subpath: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Синхронная проверка, что peer установлен (без загрузки); вызывается на верхнем уровне модуля подпути без
|
|
13
|
+
* top-level await. Сам peer загружается потом ленивым `import()`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function requirePeer(library: string, subpath: string): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Загрузка опционального peer с текстом ошибки AC-1.2 (prd.md §3, NFR-3; ARCHITECTURE.md «Правила зависимостей»).
|
|
2
|
+
// Вызывается top-level await в модуле подпути: статический импорт отсутствующего пакета упал бы при связывании ESM
|
|
3
|
+
// с ERR_MODULE_NOT_FOUND до любого кода. requirePeer — синхронная проверка при загрузке модуля подпути без top-level
|
|
4
|
+
// await (/database, /idempotency: drizzle-kit загружает схему сервиса через require — П15, KT-20 п. 2).
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
/** Текст ошибки AC-1.2. */
|
|
7
|
+
export function missingPeerMessage(library, subpath) {
|
|
8
|
+
return `@verdeect/kit/${subpath} требует ${library} — добавьте в зависимости сервиса`;
|
|
9
|
+
}
|
|
10
|
+
/** Не найден сам пакет library; отсутствующая вложенная зависимость peer называет другой пакет. */
|
|
11
|
+
function isMissingPackage(error, library) {
|
|
12
|
+
if (typeof error !== 'object' || error === null || !('code' in error)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (error.code !== 'ERR_MODULE_NOT_FOUND' && error.code !== 'MODULE_NOT_FOUND') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const message = 'message' in error ? String(error.message) : '';
|
|
19
|
+
return message.includes(`'${library}'`) || message.includes(`"${library}"`);
|
|
20
|
+
}
|
|
21
|
+
/** Загружает peer через importer; отсутствие самого пакета → ошибка AC-1.2, прочее — как есть. */
|
|
22
|
+
export async function loadPeerWith(importer, library, subpath) {
|
|
23
|
+
try {
|
|
24
|
+
return (await importer(library));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (isMissingPackage(error, library)) {
|
|
28
|
+
throw new Error(missingPeerMessage(library, subpath), { cause: error });
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Опциональный peer подпути; вызывается top-level await в модуле подпути. */
|
|
34
|
+
export function loadPeer(library, subpath) {
|
|
35
|
+
return loadPeerWith((specifier) => import(specifier), library, subpath);
|
|
36
|
+
}
|
|
37
|
+
/** Синхронная проверка peer через resolver; отсутствие самого пакета → ошибка AC-1.2, прочее — как есть. */
|
|
38
|
+
export function requirePeerWith(resolver, library, subpath) {
|
|
39
|
+
try {
|
|
40
|
+
resolver(library);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isMissingPackage(error, library)) {
|
|
44
|
+
throw new Error(missingPeerMessage(library, subpath), { cause: error });
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Синхронная проверка, что peer установлен (без загрузки); вызывается на верхнем уровне модуля подпути без
|
|
51
|
+
* top-level await. Сам peer загружается потом ленивым `import()`.
|
|
52
|
+
*/
|
|
53
|
+
export function requirePeer(library, subpath) {
|
|
54
|
+
const require = createRequire(import.meta.url);
|
|
55
|
+
requirePeerWith((specifier) => require.resolve(specifier), library, subpath);
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Нарушение уникальности (23505); `constraint` — имя ограничения первого такого узла цепочки. */
|
|
2
|
+
export declare function isUniqueViolation(error: unknown, constraint?: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Ошибка соединения с PostgreSQL: SQLSTATE `08xxx`, `57P01`–`57P03`, системные коды обрыва сети, сообщения pg
|
|
5
|
+
* «Connection terminated…», «terminating connection…» и «Client has encountered a connection error…» — в ошибке или
|
|
6
|
+
* её `cause`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isConnectionError(error: unknown): boolean;
|
|
9
|
+
/** Срок ожидания клиента пула истёк (тексты pg-pool) — в ошибке или её `cause`, в том числе внутри `DomainError`. */
|
|
10
|
+
export declare function isPoolTimeout(error: unknown): boolean;
|
|
11
|
+
/** Имя пула `/database`: основной или пул чтения (метка `pool` метрики срока, AC-4.5). */
|
|
12
|
+
export type KitPoolName = 'main' | 'read';
|
|
13
|
+
/**
|
|
14
|
+
* Помечает узел срока пула в цепочке `cause` именем пула. `true` — метка поставлена сейчас (вызывающий
|
|
15
|
+
* учитывает срок один раз); `false` — узла срока нет или он уже помечен.
|
|
16
|
+
*/
|
|
17
|
+
export declare function tagPoolTimeout(error: unknown, pool: KitPoolName): boolean;
|
|
18
|
+
/** Имя пула из метки `tagPoolTimeout` первого помеченного узла цепочки или `null`. */
|
|
19
|
+
export declare function poolTimeoutOf(error: unknown): KitPoolName | null;
|
|
20
|
+
/** SQLSTATE первого узла цепочки `cause` (до 10 уровней) или `null` — например, ошибка pg в `cause` `DrizzleQueryError`. */
|
|
21
|
+
export declare function sqlStateOf(error: unknown): string | null;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// isUniqueViolation, isConnectionError, isPoolTimeout — по форме объекта, обход cause, без pg и drizzle-orm (prd.md §3.3;
|
|
2
|
+
// rules/base.md). Эталоны: isUniqueViolation — I pg-errors.ts; isPoolTimeout — O pool-client.ts (сообщения);
|
|
3
|
+
// isConnectionError — по PRD (у I/O — instanceof DrizzleQueryError, не переносится; формы — KT-15 п. 9).
|
|
4
|
+
// tagPoolTimeout/poolTimeoutOf — метка пула /database: срок учтён (решение E3, KT-19 п. 3, AC-4.5).
|
|
5
|
+
/** Глубина обхода `cause` — как у `sanitizeError` (§3.2). */
|
|
6
|
+
const MAX_CAUSE_DEPTH = 10;
|
|
7
|
+
/** SQLSTATE нарушения уникальности (PostgreSQL, класс 23). */
|
|
8
|
+
const UNIQUE_VIOLATION = '23505';
|
|
9
|
+
/** Тексты pg-pool при истечении срока ожидания клиента (O pool-client.ts POOL_TIMEOUT_MESSAGES). */
|
|
10
|
+
const POOL_TIMEOUT_MESSAGES = new Set([
|
|
11
|
+
'timeout exceeded when trying to connect',
|
|
12
|
+
'Connection terminated due to connection timeout',
|
|
13
|
+
]);
|
|
14
|
+
/** SQLSTATE прекращения работы сервера: admin_shutdown, crash_shutdown, cannot_connect_now (класс 57P). */
|
|
15
|
+
const SHUTDOWN_SQLSTATES = new Set(['57P01', '57P02', '57P03']);
|
|
16
|
+
/** Системные коды Node обрыва и отказа сети. */
|
|
17
|
+
const NETWORK_CODES = new Set([
|
|
18
|
+
'ECONNRESET',
|
|
19
|
+
'ECONNREFUSED',
|
|
20
|
+
'ETIMEDOUT',
|
|
21
|
+
'EPIPE',
|
|
22
|
+
'ENOTFOUND',
|
|
23
|
+
]);
|
|
24
|
+
/** Начала сообщений pg при обрыве соединения (последнее — запрос на клиенте после обрыва, AC-4.10). */
|
|
25
|
+
const CONNECTION_MESSAGE_PREFIXES = [
|
|
26
|
+
'Connection terminated',
|
|
27
|
+
'terminating connection',
|
|
28
|
+
'Client has encountered a connection error',
|
|
29
|
+
];
|
|
30
|
+
/** Ошибка и её `cause` до 10 уровней: только объекты, цикл не зацикливает. */
|
|
31
|
+
function causeChain(error) {
|
|
32
|
+
const chain = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
let current = error;
|
|
35
|
+
while (chain.length < MAX_CAUSE_DEPTH &&
|
|
36
|
+
typeof current === 'object' &&
|
|
37
|
+
current !== null &&
|
|
38
|
+
!seen.has(current)) {
|
|
39
|
+
seen.add(current);
|
|
40
|
+
chain.push(current);
|
|
41
|
+
current = current.cause;
|
|
42
|
+
}
|
|
43
|
+
return chain;
|
|
44
|
+
}
|
|
45
|
+
/** Нарушение уникальности (23505); `constraint` — имя ограничения первого такого узла цепочки. */
|
|
46
|
+
export function isUniqueViolation(error, constraint) {
|
|
47
|
+
const node = causeChain(error).find((item) => item.code === UNIQUE_VIOLATION);
|
|
48
|
+
if (node === undefined) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return constraint === undefined || node.constraint === constraint;
|
|
52
|
+
}
|
|
53
|
+
function isConnectionNode(node) {
|
|
54
|
+
if (typeof node.code === 'string') {
|
|
55
|
+
if (node.code.startsWith('08') && node.code.length === 5) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (SHUTDOWN_SQLSTATES.has(node.code) || NETWORK_CODES.has(node.code)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const message = node.message;
|
|
63
|
+
return (typeof message === 'string' &&
|
|
64
|
+
CONNECTION_MESSAGE_PREFIXES.some((prefix) => message.startsWith(prefix)));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ошибка соединения с PostgreSQL: SQLSTATE `08xxx`, `57P01`–`57P03`, системные коды обрыва сети, сообщения pg
|
|
68
|
+
* «Connection terminated…», «terminating connection…» и «Client has encountered a connection error…» — в ошибке или
|
|
69
|
+
* её `cause`.
|
|
70
|
+
*/
|
|
71
|
+
export function isConnectionError(error) {
|
|
72
|
+
return causeChain(error).some(isConnectionNode);
|
|
73
|
+
}
|
|
74
|
+
/** Срок ожидания клиента пула истёк (тексты pg-pool) — в ошибке или её `cause`, в том числе внутри `DomainError`. */
|
|
75
|
+
export function isPoolTimeout(error) {
|
|
76
|
+
return causeChain(error).some(isPoolTimeoutNode);
|
|
77
|
+
}
|
|
78
|
+
/** Неперечисляемая метка «срок учтён пулом» на узле ошибки срока. */
|
|
79
|
+
const POOL_TAG = Symbol.for('verdeect-kit.pool');
|
|
80
|
+
function isPoolTimeoutNode(node) {
|
|
81
|
+
return typeof node.message === 'string' && POOL_TIMEOUT_MESSAGES.has(node.message);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Помечает узел срока пула в цепочке `cause` именем пула. `true` — метка поставлена сейчас (вызывающий
|
|
85
|
+
* учитывает срок один раз); `false` — узла срока нет или он уже помечен.
|
|
86
|
+
*/
|
|
87
|
+
export function tagPoolTimeout(error, pool) {
|
|
88
|
+
const node = causeChain(error).find(isPoolTimeoutNode);
|
|
89
|
+
if (node === undefined || Object.prototype.hasOwnProperty.call(node, POOL_TAG)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
Object.defineProperty(node, POOL_TAG, { value: pool, enumerable: false });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/** Имя пула из метки `tagPoolTimeout` первого помеченного узла цепочки или `null`. */
|
|
96
|
+
export function poolTimeoutOf(error) {
|
|
97
|
+
for (const node of causeChain(error)) {
|
|
98
|
+
const tag = node[POOL_TAG];
|
|
99
|
+
if (tag === 'main' || tag === 'read') {
|
|
100
|
+
return tag;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** SQLSTATE: 5 знаков `0-9A-Z` с цифрой (системные коды Node вроде `EPIPE` цифр не содержат). */
|
|
106
|
+
const SQLSTATE = /^(?=.*\d)[0-9A-Z]{5}$/;
|
|
107
|
+
/** SQLSTATE первого узла цепочки `cause` (до 10 уровней) или `null` — например, ошибка pg в `cause` `DrizzleQueryError`. */
|
|
108
|
+
export function sqlStateOf(error) {
|
|
109
|
+
for (const node of causeChain(error)) {
|
|
110
|
+
if (typeof node.code === 'string' && SQLSTATE.test(node.code)) {
|
|
111
|
+
return node.code;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Запоминает разобранное окружение схемы (пишет bootstrapService). */
|
|
2
|
+
export declare function setPreparsedEnv(schema: object, env: Record<string, unknown>): void;
|
|
3
|
+
/** Возвращает и удаляет разобранное окружение схемы; нет — `undefined`. */
|
|
4
|
+
export declare function takePreparsedEnv(schema: object): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Окружение, разобранное bootstrapService до создания приложения (prd.md §3.1, §3.5, AC-3.13). KitConfigModule забирает
|
|
2
|
+
// его при сборке контекста и не разбирает повторно. Ключ — объект схемы: тот же env в bootstrapService({env}) и
|
|
3
|
+
// KitConfigModule.forRoot({schema}). Значение отдаётся одной сборке: следующая разбирает process.env сама (AC-1.7).
|
|
4
|
+
const preparsed = new WeakMap();
|
|
5
|
+
/** Запоминает разобранное окружение схемы (пишет bootstrapService). */
|
|
6
|
+
export function setPreparsedEnv(schema, env) {
|
|
7
|
+
preparsed.set(schema, env);
|
|
8
|
+
}
|
|
9
|
+
/** Возвращает и удаляет разобранное окружение схемы; нет — `undefined`. */
|
|
10
|
+
export function takePreparsedEnv(schema) {
|
|
11
|
+
const env = preparsed.get(schema);
|
|
12
|
+
preparsed.delete(schema);
|
|
13
|
+
return env;
|
|
14
|
+
}
|