@coopenomics/extension-kit 2026.8.18-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.
@@ -0,0 +1,1071 @@
1
+ 'use strict';
2
+
3
+ const typeorm = require('typeorm');
4
+ const crypto = require('crypto');
5
+ const graphql = require('@nestjs/graphql');
6
+ const common = require('@nestjs/common');
7
+ const typeorm$1 = require('@nestjs/typeorm');
8
+
9
+ var __defProp$a = Object.defineProperty;
10
+ var __getOwnPropDesc$a = Object.getOwnPropertyDescriptor;
11
+ var __decorateClass$a = (decorators, target, key, kind) => {
12
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$a(target, key) : target;
13
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
14
+ if (decorator = decorators[i])
15
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
16
+ if (kind && result)
17
+ __defProp$a(target, key, result);
18
+ return result;
19
+ };
20
+ class BaseTypeormEntity {
21
+ /**
22
+ * Получить имя таблицы для сущности
23
+ * ДОЛЖЕН БЫТЬ ПЕРЕОПРЕДЕЛЕН в каждом наследнике!
24
+ */
25
+ static getTableName() {
26
+ throw new Error("getTableName() must be implemented in subclass");
27
+ }
28
+ }
29
+ __decorateClass$a([
30
+ typeorm.PrimaryGeneratedColumn("uuid")
31
+ ], BaseTypeormEntity.prototype, "_id", 2);
32
+ __decorateClass$a([
33
+ typeorm.Column({ type: "integer", default: 0 })
34
+ ], BaseTypeormEntity.prototype, "block_num", 2);
35
+ __decorateClass$a([
36
+ typeorm.Column({ type: "boolean", default: false })
37
+ ], BaseTypeormEntity.prototype, "present", 2);
38
+ __decorateClass$a([
39
+ typeorm.Column({ type: "varchar" })
40
+ ], BaseTypeormEntity.prototype, "status", 2);
41
+ __decorateClass$a([
42
+ typeorm.CreateDateColumn({ type: "timestamp" })
43
+ ], BaseTypeormEntity.prototype, "_created_at", 2);
44
+ __decorateClass$a([
45
+ typeorm.UpdateDateColumn({ type: "timestamp" })
46
+ ], BaseTypeormEntity.prototype, "_updated_at", 2);
47
+
48
+ class BaseDomainEntity {
49
+ // Дата последнего обновления в базе данных
50
+ /**
51
+ * Конструктор базового класса
52
+ *
53
+ * @param databaseData - данные из базы данных
54
+ * @param defaultStatus - статус по умолчанию, если не указан
55
+ */
56
+ constructor(databaseData, defaultStatus) {
57
+ this._id = databaseData._id === "" ? crypto.randomUUID().toString() : databaseData._id;
58
+ this.block_num = databaseData.block_num ?? 0;
59
+ this.present = databaseData.present;
60
+ this.status = databaseData.status ?? defaultStatus;
61
+ this._created_at = databaseData._created_at ? new Date(databaseData._created_at) : /* @__PURE__ */ new Date();
62
+ this._updated_at = databaseData._updated_at ? new Date(databaseData._updated_at) : /* @__PURE__ */ new Date();
63
+ }
64
+ /**
65
+ * Обновление базовых данных сущности
66
+ */
67
+ updateBase(data) {
68
+ if (data.block_num !== void 0)
69
+ this.block_num = data.block_num;
70
+ if (data.present !== void 0)
71
+ this.present = data.present;
72
+ if (data.status !== void 0)
73
+ this.status = data.status;
74
+ if (data._created_at !== void 0)
75
+ this._created_at = data._created_at;
76
+ if (data._updated_at !== void 0)
77
+ this._updated_at = data._updated_at;
78
+ }
79
+ }
80
+
81
+ var __defProp$9 = Object.defineProperty;
82
+ var __getOwnPropDesc$9 = Object.getOwnPropertyDescriptor;
83
+ var __decorateClass$9 = (decorators, target, key, kind) => {
84
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$9(target, key) : target;
85
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
86
+ if (decorator = decorators[i])
87
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
88
+ if (kind && result)
89
+ __defProp$9(target, key, result);
90
+ return result;
91
+ };
92
+ exports.BaseOutputDTO = class BaseOutputDTO {
93
+ };
94
+ __decorateClass$9([
95
+ graphql.Field(() => String, {
96
+ description: "\u0412\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0438\u0439 ID \u0431\u0430\u0437\u044B \u0434\u0430\u043D\u043D\u044B\u0445"
97
+ })
98
+ ], exports.BaseOutputDTO.prototype, "_id", 2);
99
+ __decorateClass$9([
100
+ graphql.Field(() => Boolean, {
101
+ description: "\u0424\u043B\u0430\u0433 \u043F\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044F \u0437\u0430\u043F\u0438\u0441\u0438 \u0432 \u0431\u043B\u043E\u043A\u0447\u0435\u0439\u043D\u0435"
102
+ })
103
+ ], exports.BaseOutputDTO.prototype, "present", 2);
104
+ __decorateClass$9([
105
+ graphql.Field(() => Number, {
106
+ nullable: true,
107
+ description: "\u041D\u043E\u043C\u0435\u0440 \u0431\u043B\u043E\u043A\u0430 \u043A\u0440\u0430\u0439\u043D\u0435\u0439 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 \u0431\u043B\u043E\u043A\u0447\u0435\u0439\u043D\u043E\u043C"
108
+ })
109
+ ], exports.BaseOutputDTO.prototype, "block_num", 2);
110
+ __decorateClass$9([
111
+ graphql.Field(() => Date, {
112
+ description: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F \u0437\u0430\u043F\u0438\u0441\u0438"
113
+ })
114
+ ], exports.BaseOutputDTO.prototype, "_created_at", 2);
115
+ __decorateClass$9([
116
+ graphql.Field(() => Date, {
117
+ description: "\u0414\u0430\u0442\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0433\u043E \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u0437\u0430\u043F\u0438\u0441\u0438"
118
+ })
119
+ ], exports.BaseOutputDTO.prototype, "_updated_at", 2);
120
+ exports.BaseOutputDTO = __decorateClass$9([
121
+ graphql.ObjectType("BaseEntity", {
122
+ description: "\u0411\u0430\u0437\u043E\u0432\u044B\u0435 \u043F\u043E\u043B\u044F \u0441\u0443\u0449\u043D\u043E\u0441\u0442\u0438"
123
+ })
124
+ ], exports.BaseOutputDTO);
125
+
126
+ class AbstractBlockchainDeltaMapper {
127
+ /**
128
+ * Получение всех возможных паттернов событий для подписки
129
+ * Возвращает массив паттернов типа "delta::contract::table"
130
+ */
131
+ getAllEventPatterns() {
132
+ const patterns = [];
133
+ const supportedContracts = this.getSupportedContractNames();
134
+ const supportedTables = this.getSupportedTableNames();
135
+ for (const contractName of supportedContracts) {
136
+ for (const tableName of supportedTables) {
137
+ patterns.push(`delta::${contractName}::${tableName}`);
138
+ }
139
+ }
140
+ return patterns;
141
+ }
142
+ }
143
+
144
+ const FORK_AWARE_MARKER = Symbol.for("mono.controller.shared.sync.ForkAware");
145
+ function isForkAware(candidate) {
146
+ if (candidate == null)
147
+ return false;
148
+ const obj = candidate;
149
+ return obj[FORK_AWARE_MARKER] === true && typeof obj.handleFork === "function";
150
+ }
151
+
152
+ class UnsupportedContractVersionError extends Error {
153
+ constructor(entityName, context) {
154
+ super(
155
+ `Unsupported contract version while mapping delta for ${entityName}: ${JSON.stringify(context)}`
156
+ );
157
+ this.entityName = entityName;
158
+ this.context = context;
159
+ this.name = "UnsupportedContractVersionError";
160
+ }
161
+ }
162
+
163
+ const DEFAULTS = {
164
+ unsupportedVersionStrict: false
165
+ };
166
+ let policy = DEFAULTS;
167
+ function configureSyncPolicy(next) {
168
+ policy = { ...DEFAULTS, ...next };
169
+ }
170
+ function syncPolicy() {
171
+ return policy;
172
+ }
173
+
174
+ var __defProp$8 = Object.defineProperty;
175
+ var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
176
+ var __decorateClass$8 = (decorators, target, key, kind) => {
177
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$8(target, key) : target;
178
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
179
+ if (decorator = decorators[i])
180
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
181
+ if (kind && result)
182
+ __defProp$8(target, key, result);
183
+ return result;
184
+ };
185
+ var _a;
186
+ exports.AbstractEntitySyncService = class AbstractEntitySyncService {
187
+ constructor(repository, mapper, logger) {
188
+ this.repository = repository;
189
+ this.mapper = mapper;
190
+ this.logger = logger;
191
+ /**
192
+ * Symbol-маркер для ForkRegistryService (Story 4.1). Все 20+ наследников
193
+ * автоматически попадают в реестр через bootstrap-сканирование Discovery —
194
+ * без правок их onModuleInit.
195
+ */
196
+ this[_a] = true;
197
+ this.logger.setContext(`${this.constructor.name}`);
198
+ }
199
+ static {
200
+ _a = FORK_AWARE_MARKER;
201
+ }
202
+ /**
203
+ * Обработка дельты блокчейна
204
+ */
205
+ async processDelta(delta) {
206
+ try {
207
+ this.logger.debug(`Processing ${this.entityName} delta for table ${delta.table} with key ${delta.primary_key}`);
208
+ const syncValue = this.mapper.extractSyncValue(delta);
209
+ const syncKey = this.mapper.extractSyncKey();
210
+ const blockchainData = this.mapper.mapDeltaToBlockchainData(delta);
211
+ if (!blockchainData) {
212
+ const ctx = {
213
+ contract: delta.contract ?? delta.code,
214
+ table: delta.table,
215
+ primary_key: delta.primary_key,
216
+ block_num: Number(delta.block_num)
217
+ };
218
+ this.logger.error(
219
+ `UNSUPPORTED_CONTRACT_VERSION: mapDeltaToBlockchainData returned null for ${this.entityName} ${syncValue}`,
220
+ { entity: this.entityName, syncValue, ...ctx }
221
+ );
222
+ if (syncPolicy().unsupportedVersionStrict) {
223
+ throw new UnsupportedContractVersionError(this.entityName, ctx);
224
+ }
225
+ return null;
226
+ }
227
+ const blockNum = Number(delta.block_num);
228
+ const present = delta.present !== false;
229
+ return await this.handleSyncDelta(syncKey, syncValue, blockchainData, blockNum, present);
230
+ } catch (error) {
231
+ if (error instanceof UnsupportedContractVersionError)
232
+ throw error;
233
+ this.logger.error(`Error processing ${this.entityName} delta: ${error.message}`, error.stack);
234
+ return null;
235
+ }
236
+ }
237
+ /**
238
+ * Обработка создания/обновления сущности
239
+ */
240
+ async handleSyncDelta(syncKey, syncValue, blockchainData, blockNum, present = true) {
241
+ const existingEntity = await this.repository.findBySyncKey(syncKey, syncValue);
242
+ if (existingEntity) {
243
+ const currentBlockNum = existingEntity.getBlockNum();
244
+ if (currentBlockNum && blockNum < currentBlockNum) {
245
+ this.logger.debug(
246
+ `Skipping outdated update for ${this.entityName} ${syncValue}: block ${blockNum} <= ${currentBlockNum}`
247
+ );
248
+ return {
249
+ created: false,
250
+ updated: false,
251
+ blockchainId: syncValue,
252
+ blockNum: currentBlockNum
253
+ };
254
+ }
255
+ existingEntity.updateFromBlockchain(blockchainData, blockNum, present);
256
+ await this.repository.update(existingEntity);
257
+ this.logger.debug(`\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D ${this.entityName} ${syncValue} \u0432 \u0431\u043B\u043E\u043A\u0435 ${blockNum}`);
258
+ return {
259
+ created: false,
260
+ updated: true,
261
+ blockchainId: syncValue,
262
+ blockNum
263
+ };
264
+ } else {
265
+ await this.repository.createIfNotExists(blockchainData, blockNum, present);
266
+ this.logger.debug(`\u0421\u043E\u0437\u0434\u0430\u043D ${this.entityName} ${syncValue} \u0432 \u0431\u043B\u043E\u043A\u0435 ${blockNum}`);
267
+ return {
268
+ created: true,
269
+ updated: false,
270
+ blockchainId: syncValue,
271
+ blockNum
272
+ };
273
+ }
274
+ }
275
+ /**
276
+ * Обработка удаления сущности
277
+ */
278
+ async handleEntityDeletion(syncValue, blockNum) {
279
+ this.logger.debug(`Entity ${this.entityName} ${syncValue} was deleted at block ${blockNum}`);
280
+ return {
281
+ created: false,
282
+ updated: false,
283
+ blockchainId: syncValue,
284
+ blockNum
285
+ };
286
+ }
287
+ /**
288
+ * Обработка форка — архивирование снесённых сущностей + восстановление из versions
289
+ * + архивирование инвалидированных версий.
290
+ *
291
+ * Story 4.1: ошибки больше НЕ глотаются — обязательный re-throw для контракта
292
+ * sequential ForkRegistry.runAll (INV-T03). Если rollback упадёт — parser2 не
293
+ * ACK'нет fork-event, повторная доставка пересыграет цепочку. Уже отработавшие
294
+ * syncer'ы в цепи будут no-op (versions уже подняты), сбойный — попробует ещё раз.
295
+ *
296
+ * Story 4.4: hard-delete заменён на «архив + delete» атомарно. Порядок:
297
+ * 1) archiveInvalidatedSince — live-ряды WHERE block_num > N переезжают в
298
+ * invalidated_entities, оригинал удаляется (одна транзакция).
299
+ * 2) restoreFromVersions — поднять previous_data из ещё-живых entity_versions.
300
+ * 3) archiveInvalidatedVersionsSince — entity_versions WHERE entity_table=... AND
301
+ * block_num > N переезжают в invalidated_entity_versions, оригинал удаляется.
302
+ * Запускается ПОСЛЕ restore, иначе restore не сможет прочитать живые версии.
303
+ * Если репо не реализует archive методы (off-chain) — graceful no-op + fallback
304
+ * на старую findByBlockNumGreaterThan/deleteByBlockNumGreaterThan для бэк-совместимости.
305
+ */
306
+ async handleFork(forkBlockNum, forkEventId) {
307
+ this.logger.log(`Handling fork for ${this.entityName} at block ${forkBlockNum} (eventId=${forkEventId ?? "n/a"})`);
308
+ let archivedLive = 0;
309
+ if (this.repository.archiveInvalidatedSince) {
310
+ archivedLive = await this.repository.archiveInvalidatedSince(forkBlockNum, forkEventId);
311
+ this.logger.log(
312
+ `\u0410\u0440\u0445\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u043E ${archivedLive} live-\u0440\u044F\u0434\u043E\u0432 ${this.entityName} \u043D\u0430 \u0444\u043E\u0440\u043A\u0435 ${forkBlockNum}`
313
+ );
314
+ } else {
315
+ const affected = await this.repository.findByBlockNumGreaterThan(forkBlockNum);
316
+ await this.repository.deleteByBlockNumGreaterThan(forkBlockNum);
317
+ archivedLive = affected.length;
318
+ this.logger.warn(
319
+ `${this.entityName}: archiveInvalidatedSince \u043D\u0435 \u0440\u0435\u0430\u043B\u0438\u0437\u043E\u0432\u0430\u043D \u2014 fallback \u043D\u0430 hard-delete (${archivedLive} \u0440\u044F\u0434\u043E\u0432)`
320
+ );
321
+ }
322
+ if (this.repository.restoreFromVersions) {
323
+ await this.repository.restoreFromVersions(forkBlockNum);
324
+ this.logger.log(`Restored ${this.entityName} entities from versions after fork at block ${forkBlockNum}`);
325
+ }
326
+ if (this.repository.archiveInvalidatedVersionsSince) {
327
+ const archivedVersions = await this.repository.archiveInvalidatedVersionsSince(forkBlockNum, forkEventId);
328
+ this.logger.log(
329
+ `\u0410\u0440\u0445\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u043E ${archivedVersions} \u0432\u0435\u0440\u0441\u0438\u0439 ${this.entityName} \u043D\u0430 \u0444\u043E\u0440\u043A\u0435 ${forkBlockNum}`
330
+ );
331
+ }
332
+ await this.afterForkProcessing(forkBlockNum, []);
333
+ }
334
+ /**
335
+ * Дополнительные действия после обработки форка
336
+ * Может быть переопределен в наследниках
337
+ */
338
+ async afterForkProcessing(_forkBlockNum, _affectedEntities) {
339
+ }
340
+ /**
341
+ * Получение всех возможных имен событий для подписки
342
+ */
343
+ getAllEventPatterns() {
344
+ return this.mapper.getAllEventPatterns();
345
+ }
346
+ /**
347
+ * Получение всех поддерживаемых таблиц и контрактов для логирования
348
+ */
349
+ getSupportedVersions() {
350
+ return {
351
+ contracts: this.mapper.getSupportedContractNames(),
352
+ tables: this.mapper.getSupportedTableNames()
353
+ };
354
+ }
355
+ /**
356
+ * Получение имени события для подписки на форки
357
+ */
358
+ getForkEventPattern() {
359
+ return "fork::*";
360
+ }
361
+ };
362
+ exports.AbstractEntitySyncService = __decorateClass$8([
363
+ common.Injectable()
364
+ ], exports.AbstractEntitySyncService);
365
+
366
+ var __defProp$7 = Object.defineProperty;
367
+ var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
368
+ var __decorateClass$7 = (decorators, target, key, kind) => {
369
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$7(target, key) : target;
370
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
371
+ if (decorator = decorators[i])
372
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
373
+ if (kind && result)
374
+ __defProp$7(target, key, result);
375
+ return result;
376
+ };
377
+ exports.EntityVersionTypeormEntity = class EntityVersionTypeormEntity {
378
+ };
379
+ __decorateClass$7([
380
+ typeorm.PrimaryGeneratedColumn("uuid")
381
+ ], exports.EntityVersionTypeormEntity.prototype, "id", 2);
382
+ __decorateClass$7([
383
+ typeorm.Column({ type: "varchar", length: 100 })
384
+ ], exports.EntityVersionTypeormEntity.prototype, "entity_table", 2);
385
+ __decorateClass$7([
386
+ typeorm.Column({ type: "varchar", length: 36 })
387
+ ], exports.EntityVersionTypeormEntity.prototype, "entity_id", 2);
388
+ __decorateClass$7([
389
+ typeorm.Column({ type: "jsonb" })
390
+ ], exports.EntityVersionTypeormEntity.prototype, "previous_data", 2);
391
+ __decorateClass$7([
392
+ typeorm.Column({ type: "integer", nullable: true })
393
+ ], exports.EntityVersionTypeormEntity.prototype, "block_num", 2);
394
+ __decorateClass$7([
395
+ typeorm.Column({ type: "varchar", length: 50 })
396
+ ], exports.EntityVersionTypeormEntity.prototype, "change_type", 2);
397
+ __decorateClass$7([
398
+ typeorm.Column({ type: "jsonb", nullable: true })
399
+ ], exports.EntityVersionTypeormEntity.prototype, "metadata", 2);
400
+ __decorateClass$7([
401
+ typeorm.CreateDateColumn({ type: "timestamp" })
402
+ ], exports.EntityVersionTypeormEntity.prototype, "created_at", 2);
403
+ exports.EntityVersionTypeormEntity = __decorateClass$7([
404
+ typeorm.Entity("entity_versions"),
405
+ typeorm.Index("idx_entity_versions_entity_table_id", ["entity_table", "entity_id"]),
406
+ typeorm.Index("idx_entity_versions_block_num", ["block_num"])
407
+ ], exports.EntityVersionTypeormEntity);
408
+
409
+ var __defProp$6 = Object.defineProperty;
410
+ var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
411
+ var __decorateClass$6 = (decorators, target, key, kind) => {
412
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
413
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
414
+ if (decorator = decorators[i])
415
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
416
+ if (kind && result)
417
+ __defProp$6(target, key, result);
418
+ return result;
419
+ };
420
+ var __decorateParam$3 = (index, decorator) => (target, key) => decorator(target, key, index);
421
+ exports.EntityVersionRepository = class EntityVersionRepository {
422
+ constructor(repository) {
423
+ this.repository = repository;
424
+ }
425
+ /**
426
+ * Соединение, на котором работает репозиторий.
427
+ *
428
+ * Нужно тем, кому требуется транзакция на несколько таблиц. Инжектить
429
+ * `DataSource` через `@InjectDataSource()` в пакете нельзя: pnpm вшивает в
430
+ * путь хэш peer-зависимостей, у пакета и контроллера оказываются разные
431
+ * экземпляры `@nestjs/typeorm`, и токен не совпадает — Nest не находит
432
+ * провайдера. Токен `@InjectRepository` такой беды не знает: он считается от
433
+ * класса сущности, а она в пакете одна.
434
+ */
435
+ get dataSource() {
436
+ return this.repository.manager.connection;
437
+ }
438
+ /**
439
+ * Сохранить предыдущую версию сущности
440
+ */
441
+ async saveVersion(entityTable, entityId, previousData, blockNum, changeType, metadata) {
442
+ const version = this.repository.create({
443
+ entity_table: entityTable,
444
+ entity_id: entityId,
445
+ previous_data: previousData,
446
+ block_num: blockNum,
447
+ change_type: changeType,
448
+ metadata
449
+ });
450
+ return await this.repository.save(version);
451
+ }
452
+ /**
453
+ * Получить последнюю версию сущности до указанного блока
454
+ */
455
+ async getLastVersionBeforeBlock(entityTable, entityId, blockNum) {
456
+ return await this.repository.createQueryBuilder("version").where("version.entity_table = :entityTable", { entityTable }).andWhere("version.entity_id = :entityId", { entityId }).andWhere("version.block_num <= :blockNum", { blockNum }).orderBy("version.block_num", "DESC").addOrderBy("version.created_at", "DESC").getOne();
457
+ }
458
+ /**
459
+ * Удалить все версии после указанного блока (локальные изменения остаются)
460
+ */
461
+ async deleteVersionsAfterBlock(blockNum) {
462
+ const result = await this.repository.createQueryBuilder().delete().from(exports.EntityVersionTypeormEntity).where("block_num > :blockNum AND block_num IS NOT NULL", { blockNum }).execute();
463
+ return result.affected || 0;
464
+ }
465
+ /**
466
+ * Получить все версии для сущностей, которые нужно восстановить
467
+ */
468
+ async getVersionsForRecovery(entityTable, maxBlockNum) {
469
+ return await this.repository.createQueryBuilder("version").where("version.entity_table = :entityTable", { entityTable }).andWhere("(version.block_num <= :maxBlockNum OR version.block_num IS NULL)", { maxBlockNum }).orderBy("version.entity_table", "ASC").addOrderBy("version.entity_id", "ASC").addOrderBy("version.block_num", "DESC").addOrderBy("version.created_at", "DESC").getMany();
470
+ }
471
+ /**
472
+ * Очистить версии для указанной сущности
473
+ */
474
+ async clearVersionsForEntity(entityTable, entityId) {
475
+ const result = await this.repository.createQueryBuilder().delete().from(exports.EntityVersionTypeormEntity).where("entity_table = :entityTable", { entityTable }).andWhere("entity_id = :entityId", { entityId }).execute();
476
+ return result.affected || 0;
477
+ }
478
+ };
479
+ exports.EntityVersionRepository = __decorateClass$6([
480
+ common.Injectable(),
481
+ __decorateParam$3(0, typeorm$1.InjectRepository(exports.EntityVersionTypeormEntity))
482
+ ], exports.EntityVersionRepository);
483
+
484
+ var __defProp$5 = Object.defineProperty;
485
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
486
+ var __decorateClass$5 = (decorators, target, key, kind) => {
487
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
488
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
489
+ if (decorator = decorators[i])
490
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
491
+ if (kind && result)
492
+ __defProp$5(target, key, result);
493
+ return result;
494
+ };
495
+ exports.InvalidatedEntityTypeormEntity = class InvalidatedEntityTypeormEntity {
496
+ };
497
+ __decorateClass$5([
498
+ typeorm.PrimaryGeneratedColumn("uuid")
499
+ ], exports.InvalidatedEntityTypeormEntity.prototype, "id", 2);
500
+ __decorateClass$5([
501
+ typeorm.Column({ type: "varchar", length: 100 })
502
+ ], exports.InvalidatedEntityTypeormEntity.prototype, "entity_table", 2);
503
+ __decorateClass$5([
504
+ typeorm.Column({ type: "varchar", length: 64 })
505
+ ], exports.InvalidatedEntityTypeormEntity.prototype, "entity_id", 2);
506
+ __decorateClass$5([
507
+ typeorm.Column({ type: "jsonb" })
508
+ ], exports.InvalidatedEntityTypeormEntity.prototype, "data", 2);
509
+ __decorateClass$5([
510
+ typeorm.Column({ type: "integer" })
511
+ ], exports.InvalidatedEntityTypeormEntity.prototype, "invalidated_by_block", 2);
512
+ __decorateClass$5([
513
+ typeorm.Column({ type: "varchar", length: 128, nullable: true })
514
+ ], exports.InvalidatedEntityTypeormEntity.prototype, "fork_event_id", 2);
515
+ __decorateClass$5([
516
+ typeorm.CreateDateColumn({ type: "timestamp" })
517
+ ], exports.InvalidatedEntityTypeormEntity.prototype, "created_at", 2);
518
+ exports.InvalidatedEntityTypeormEntity = __decorateClass$5([
519
+ typeorm.Entity("invalidated_entities"),
520
+ typeorm.Index("idx_invalidated_entities_block", ["invalidated_by_block"]),
521
+ typeorm.Index("idx_invalidated_entities_fork_event", ["fork_event_id"]),
522
+ typeorm.Index("idx_invalidated_entities_table_id", ["entity_table", "entity_id"])
523
+ ], exports.InvalidatedEntityTypeormEntity);
524
+
525
+ var __defProp$4 = Object.defineProperty;
526
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
527
+ var __decorateClass$4 = (decorators, target, key, kind) => {
528
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
529
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
530
+ if (decorator = decorators[i])
531
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
532
+ if (kind && result)
533
+ __defProp$4(target, key, result);
534
+ return result;
535
+ };
536
+ var __decorateParam$2 = (index, decorator) => (target, key) => decorator(target, key, index);
537
+ exports.InvalidatedEntityRepository = class InvalidatedEntityRepository {
538
+ constructor(repository) {
539
+ this.repository = repository;
540
+ }
541
+ async bulkInsert(records) {
542
+ if (records.length === 0)
543
+ return 0;
544
+ const entities = records.map((r) => this.repository.create(r));
545
+ const saved = await this.repository.save(entities);
546
+ return saved.length;
547
+ }
548
+ /**
549
+ * Retention: удалить архив старше указанного блока. Делается отдельной транзакцией,
550
+ * не транзакционно с архивированием — это фоновая очистка.
551
+ */
552
+ async deleteOlderThan(minInvalidatedByBlock) {
553
+ const result = await this.repository.createQueryBuilder().delete().where("invalidated_by_block < :minInvalidatedByBlock", { minInvalidatedByBlock }).execute();
554
+ return result.affected ?? 0;
555
+ }
556
+ /**
557
+ * Forensic-read для AC «список из invalidated_entities, сгруппированный по fork_event_id».
558
+ * UI/CLI обёртка — Epic 9 (out of scope 4.4); сам repository-метод доступен из backend-кода.
559
+ */
560
+ async findGroupedByForkEventId(opts) {
561
+ const qb = this.repository.createQueryBuilder("inv").orderBy("inv.fork_event_id", "ASC").addOrderBy("inv.created_at", "DESC");
562
+ if (opts.blockNum != null) {
563
+ qb.where("inv.invalidated_by_block = :blockNum", { blockNum: opts.blockNum });
564
+ }
565
+ if (opts.limit != null) {
566
+ qb.limit(opts.limit);
567
+ }
568
+ const rows = await qb.getMany();
569
+ const grouped = /* @__PURE__ */ new Map();
570
+ for (const row of rows) {
571
+ const key = row.fork_event_id ?? null;
572
+ const bucket = grouped.get(key) ?? [];
573
+ bucket.push(row);
574
+ grouped.set(key, bucket);
575
+ }
576
+ return grouped;
577
+ }
578
+ };
579
+ exports.InvalidatedEntityRepository = __decorateClass$4([
580
+ common.Injectable(),
581
+ __decorateParam$2(0, typeorm$1.InjectRepository(exports.InvalidatedEntityTypeormEntity))
582
+ ], exports.InvalidatedEntityRepository);
583
+
584
+ var __defProp$3 = Object.defineProperty;
585
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
586
+ var __decorateClass$3 = (decorators, target, key, kind) => {
587
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
588
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
589
+ if (decorator = decorators[i])
590
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
591
+ if (kind && result)
592
+ __defProp$3(target, key, result);
593
+ return result;
594
+ };
595
+ exports.InvalidatedEntityVersionTypeormEntity = class InvalidatedEntityVersionTypeormEntity {
596
+ };
597
+ __decorateClass$3([
598
+ typeorm.PrimaryGeneratedColumn("uuid")
599
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "id", 2);
600
+ __decorateClass$3([
601
+ typeorm.Column({ type: "varchar", length: 100 })
602
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "entity_table", 2);
603
+ __decorateClass$3([
604
+ typeorm.Column({ type: "varchar", length: 64 })
605
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "entity_id", 2);
606
+ __decorateClass$3([
607
+ typeorm.Column({ type: "jsonb" })
608
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "previous_data", 2);
609
+ __decorateClass$3([
610
+ typeorm.Column({ type: "integer", nullable: true })
611
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "original_block_num", 2);
612
+ __decorateClass$3([
613
+ typeorm.Column({ type: "integer" })
614
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "invalidated_by_block", 2);
615
+ __decorateClass$3([
616
+ typeorm.Column({ type: "varchar", length: 128, nullable: true })
617
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "fork_event_id", 2);
618
+ __decorateClass$3([
619
+ typeorm.Column({ type: "varchar", length: 50 })
620
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "change_type", 2);
621
+ __decorateClass$3([
622
+ typeorm.Column({ type: "jsonb", nullable: true })
623
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "metadata", 2);
624
+ __decorateClass$3([
625
+ typeorm.CreateDateColumn({ type: "timestamp" })
626
+ ], exports.InvalidatedEntityVersionTypeormEntity.prototype, "created_at", 2);
627
+ exports.InvalidatedEntityVersionTypeormEntity = __decorateClass$3([
628
+ typeorm.Entity("invalidated_entity_versions"),
629
+ typeorm.Index("idx_invalidated_versions_block", ["invalidated_by_block"]),
630
+ typeorm.Index("idx_invalidated_versions_fork_event", ["fork_event_id"]),
631
+ typeorm.Index("idx_invalidated_versions_table_id", ["entity_table", "entity_id"])
632
+ ], exports.InvalidatedEntityVersionTypeormEntity);
633
+
634
+ var __defProp$2 = Object.defineProperty;
635
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
636
+ var __decorateClass$2 = (decorators, target, key, kind) => {
637
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
638
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
639
+ if (decorator = decorators[i])
640
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
641
+ if (kind && result)
642
+ __defProp$2(target, key, result);
643
+ return result;
644
+ };
645
+ var __decorateParam$1 = (index, decorator) => (target, key) => decorator(target, key, index);
646
+ exports.InvalidatedEntityVersionRepository = class InvalidatedEntityVersionRepository {
647
+ constructor(repository) {
648
+ this.repository = repository;
649
+ }
650
+ async bulkInsert(records) {
651
+ if (records.length === 0)
652
+ return 0;
653
+ const entities = records.map((r) => this.repository.create(r));
654
+ const saved = await this.repository.save(entities);
655
+ return saved.length;
656
+ }
657
+ async deleteOlderThan(minInvalidatedByBlock) {
658
+ const result = await this.repository.createQueryBuilder().delete().where("invalidated_by_block < :minInvalidatedByBlock", { minInvalidatedByBlock }).execute();
659
+ return result.affected ?? 0;
660
+ }
661
+ };
662
+ exports.InvalidatedEntityVersionRepository = __decorateClass$2([
663
+ common.Injectable(),
664
+ __decorateParam$1(0, typeorm$1.InjectRepository(exports.InvalidatedEntityVersionTypeormEntity))
665
+ ], exports.InvalidatedEntityVersionRepository);
666
+
667
+ var __defProp$1 = Object.defineProperty;
668
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
669
+ var __decorateClass$1 = (decorators, target, key, kind) => {
670
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
671
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
672
+ if (decorator = decorators[i])
673
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
674
+ if (kind && result)
675
+ __defProp$1(target, key, result);
676
+ return result;
677
+ };
678
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
679
+ exports.EntityVersioningService = class EntityVersioningService {
680
+ // Токены зависимостей указаны явно, а не выведены из типов: пакет собирается
681
+ // unbuild/esbuild, а esbuild не умеет `emitDecoratorMetadata`. Без `@Inject`
682
+ // Nest не нашёл бы `design:paramtypes` и упал бы на инстанцировании сервиса.
683
+ constructor(entityVersionRepository, invalidatedEntityRepository, invalidatedEntityVersionRepository) {
684
+ this.entityVersionRepository = entityVersionRepository;
685
+ this.invalidatedEntityRepository = invalidatedEntityRepository;
686
+ this.invalidatedEntityVersionRepository = invalidatedEntityVersionRepository;
687
+ }
688
+ /**
689
+ * Соединение берём у репозитория, а не инъекцией `@InjectDataSource()`:
690
+ * у пакета и контроллера разные экземпляры `@nestjs/typeorm`, и токен
691
+ * источника данных не совпал бы — Nest не нашёл бы провайдера.
692
+ */
693
+ get dataSource() {
694
+ return this.entityVersionRepository.dataSource;
695
+ }
696
+ /**
697
+ * Сохранить версию сущности перед её изменением
698
+ */
699
+ async saveVersionBeforeUpdate(repository, entityTable, updatedEntity, blockNum, changeType, metadata) {
700
+ const existingEntity = await repository.findOne({
701
+ where: { _id: updatedEntity._id }
702
+ });
703
+ if (!existingEntity) {
704
+ return;
705
+ }
706
+ await this.entityVersionRepository.saveVersion(
707
+ entityTable,
708
+ existingEntity._id,
709
+ { ...existingEntity },
710
+ // Глубокая копия предыдущих данных
711
+ blockNum,
712
+ changeType,
713
+ metadata
714
+ );
715
+ }
716
+ /**
717
+ * Восстановить версии сущностей после форка
718
+ */
719
+ async restoreVersionsAfterFork(repository, entityTable, forkBlockNum) {
720
+ const versionsToRestore = await this.entityVersionRepository.getVersionsForRecovery(entityTable, forkBlockNum);
721
+ const latestVersions = /* @__PURE__ */ new Map();
722
+ for (const version of versionsToRestore) {
723
+ const existingVersion = latestVersions.get(version.entity_id);
724
+ if (!existingVersion) {
725
+ latestVersions.set(version.entity_id, version);
726
+ continue;
727
+ }
728
+ const shouldReplace = this.shouldReplaceVersion(existingVersion, version);
729
+ if (shouldReplace) {
730
+ latestVersions.set(version.entity_id, version);
731
+ }
732
+ }
733
+ for (const [entityId, version] of latestVersions) {
734
+ const existingEntity = await repository.findOne({
735
+ where: { _id: entityId }
736
+ });
737
+ if (existingEntity) {
738
+ Object.assign(existingEntity, version.previous_data);
739
+ await repository.save(existingEntity);
740
+ } else {
741
+ const restoredEntity = repository.create(version.previous_data);
742
+ await repository.save(restoredEntity);
743
+ }
744
+ }
745
+ }
746
+ /**
747
+ * Определить, должна ли новая версия заменить существующую
748
+ */
749
+ shouldReplaceVersion(existingVersion, newVersion) {
750
+ const existingBlockNum = existingVersion.block_num;
751
+ const newBlockNum = newVersion.block_num;
752
+ if (existingBlockNum === null && newBlockNum === null) {
753
+ return newVersion.created_at > existingVersion.created_at;
754
+ }
755
+ if (newBlockNum === null && existingBlockNum !== null) {
756
+ return true;
757
+ }
758
+ if (existingBlockNum === null && newBlockNum !== null) {
759
+ return false;
760
+ }
761
+ if (existingBlockNum !== null && newBlockNum !== null) {
762
+ return newBlockNum > existingBlockNum;
763
+ }
764
+ return newVersion.created_at > existingVersion.created_at;
765
+ }
766
+ /**
767
+ * Очистить версии после успешного восстановления
768
+ */
769
+ async clearVersionsAfterBlock(blockNum) {
770
+ return await this.entityVersionRepository.deleteVersionsAfterBlock(blockNum);
771
+ }
772
+ /**
773
+ * Story 4.4: атомарно перенести live-ряды WHERE block_num > forkBlockNum в архив
774
+ * `invalidated_entities` и удалить их из исходной таблицы. Возвращает количество
775
+ * перенесённых рядов. Транзакция через DataSource — INSERT и DELETE либо оба
776
+ * успешны, либо оба откатываются.
777
+ *
778
+ * Заменяет прежнюю пару findByBlockNumGreaterThan + deleteByBlockNumGreaterThan
779
+ * в hot-path handleFork (sequence сейчас: archive → restoreFromVersions →
780
+ * archiveVersions).
781
+ */
782
+ async archiveAndDeleteLiveAfterFork(repository, entityTable, forkBlockNum, forkEventId) {
783
+ return this.dataSource.transaction(async (manager) => {
784
+ const txRepo = manager.getRepository(repository.target);
785
+ const txInvalidated = manager.getRepository(exports.InvalidatedEntityTypeormEntity);
786
+ const rows = await txRepo.createQueryBuilder("e").where("e.block_num > :forkBlockNum", { forkBlockNum }).getMany();
787
+ if (rows.length === 0)
788
+ return 0;
789
+ const archiveRecords = rows.map(
790
+ (row) => txInvalidated.create({
791
+ entity_table: entityTable,
792
+ entity_id: row._id,
793
+ data: { ...row },
794
+ invalidated_by_block: forkBlockNum,
795
+ fork_event_id: forkEventId ?? null
796
+ })
797
+ );
798
+ await txInvalidated.save(archiveRecords);
799
+ await txRepo.createQueryBuilder().delete().where("block_num > :forkBlockNum", { forkBlockNum }).execute();
800
+ return rows.length;
801
+ });
802
+ }
803
+ /**
804
+ * Story 4.4: атомарно перенести entity_versions WHERE entity_table=... AND block_num > forkBlockNum
805
+ * в архив `invalidated_entity_versions` и удалить их из entity_versions.
806
+ * Возвращает количество перенесённых рядов.
807
+ *
808
+ * Запускается ПОСЛЕ restoreFromVersions — иначе restore не сможет прочитать
809
+ * ещё-живые версии.
810
+ */
811
+ async archiveAndDeleteVersionsAfterFork(entityTable, forkBlockNum, forkEventId) {
812
+ return this.dataSource.transaction(async (manager) => {
813
+ const txVersions = manager.getRepository(exports.EntityVersionTypeormEntity);
814
+ const txArchive = manager.getRepository(exports.InvalidatedEntityVersionTypeormEntity);
815
+ const versions = await txVersions.createQueryBuilder("v").where("v.entity_table = :entityTable", { entityTable }).andWhere("v.block_num > :forkBlockNum", { forkBlockNum }).getMany();
816
+ if (versions.length === 0)
817
+ return 0;
818
+ const archiveRecords = versions.map(
819
+ (v) => txArchive.create({
820
+ entity_table: v.entity_table,
821
+ entity_id: v.entity_id,
822
+ previous_data: v.previous_data,
823
+ original_block_num: v.block_num ?? null,
824
+ invalidated_by_block: forkBlockNum,
825
+ fork_event_id: forkEventId ?? null,
826
+ change_type: v.change_type,
827
+ metadata: v.metadata ?? null
828
+ })
829
+ );
830
+ await txArchive.save(archiveRecords);
831
+ const ids = versions.map((v) => v.id);
832
+ await txVersions.createQueryBuilder().delete().from(exports.EntityVersionTypeormEntity).whereInIds(ids).execute();
833
+ return versions.length;
834
+ });
835
+ }
836
+ };
837
+ exports.EntityVersioningService = __decorateClass$1([
838
+ common.Injectable(),
839
+ __decorateParam(0, common.Inject(exports.EntityVersionRepository)),
840
+ __decorateParam(1, common.Inject(exports.InvalidatedEntityRepository)),
841
+ __decorateParam(2, common.Inject(exports.InvalidatedEntityVersionRepository))
842
+ ], exports.EntityVersioningService);
843
+
844
+ var __defProp = Object.defineProperty;
845
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
846
+ var __decorateClass = (decorators, target, key, kind) => {
847
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
848
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
849
+ if (decorator = decorators[i])
850
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
851
+ if (kind && result)
852
+ __defProp(target, key, result);
853
+ return result;
854
+ };
855
+ exports.BaseBlockchainRepository = class BaseBlockchainRepository {
856
+ constructor(repository, entityVersioningService) {
857
+ this.repository = repository;
858
+ this.entityVersioningService = entityVersioningService;
859
+ }
860
+ /**
861
+ * Получить имя таблицы сущности
862
+ */
863
+ getEntityTableName() {
864
+ const entityClass = this.repository.target;
865
+ if (typeof entityClass.getTableName === "function") {
866
+ return entityClass.getTableName();
867
+ }
868
+ return this.repository.metadata.tableName;
869
+ }
870
+ /**
871
+ * Найти сущность по кастомному ключу синхронизации
872
+ */
873
+ async findBySyncKey(syncKey, syncValue) {
874
+ const whereCondition = { [syncKey]: syncValue.toLowerCase() };
875
+ const entity = await this.repository.findOne({
876
+ where: whereCondition
877
+ });
878
+ return entity ? this.getMapper().toDomain(entity) : null;
879
+ }
880
+ /**
881
+ * Найти сущности с номером блока больше указанного
882
+ */
883
+ async findByBlockNumGreaterThan(blockNum) {
884
+ const entities = await this.repository.createQueryBuilder("entity").where("entity.block_num > :blockNum", { blockNum }).getMany();
885
+ return entities.map((entity) => this.getMapper().toDomain(entity));
886
+ }
887
+ /**
888
+ * Создать сущность если не существует
889
+ * Используется для синхронизации данных из блокчейна
890
+ */
891
+ async createIfNotExists(blockchainData, blockNum, present = true) {
892
+ const syncKey = this.getSyncKey();
893
+ const syncValue = this.extractSyncValueFromBlockchainData(blockchainData, syncKey);
894
+ const existing = await this.findBySyncKey(syncKey, syncValue);
895
+ if (existing) {
896
+ const existingBlockNum = existing.getBlockNum();
897
+ if (existingBlockNum != null && Number(blockNum) < Number(existingBlockNum)) {
898
+ return existing;
899
+ }
900
+ existing.updateFromBlockchain(blockchainData, blockNum, present);
901
+ return await this.save(existing);
902
+ } else {
903
+ const now = /* @__PURE__ */ new Date();
904
+ const minimalDatabaseData = {
905
+ _id: "",
906
+ block_num: blockNum,
907
+ present,
908
+ _created_at: now,
909
+ _updated_at: now,
910
+ [syncKey]: syncValue.toLowerCase()
911
+ // ключ синхронизации
912
+ };
913
+ const newEntity = this.createDomainEntity(minimalDatabaseData, blockchainData);
914
+ return await this.save(newEntity);
915
+ }
916
+ }
917
+ /**
918
+ * Удалить сущности с номером блока больше указанного (для обработки форков)
919
+ */
920
+ async deleteByBlockNumGreaterThan(blockNum) {
921
+ await this.repository.createQueryBuilder().delete().where("block_num > :blockNum", { blockNum }).execute();
922
+ }
923
+ /**
924
+ * Восстановить сущности из версий после форка
925
+ */
926
+ async restoreFromVersions(forkBlockNum) {
927
+ await this.entityVersioningService.restoreVersionsAfterFork(this.repository, this.getEntityTableName(), forkBlockNum);
928
+ }
929
+ /**
930
+ * Story 4.4: архивировать live-ряды WHERE block_num > forkBlockNum в invalidated_entities
931
+ * и удалить из исходной таблицы (атомарно). Возвращает count. Заменяет в hot-path
932
+ * handleFork прежнюю пару findByBlockNumGreaterThan + deleteByBlockNumGreaterThan.
933
+ */
934
+ async archiveInvalidatedSince(forkBlockNum, forkEventId) {
935
+ return this.entityVersioningService.archiveAndDeleteLiveAfterFork(
936
+ this.repository,
937
+ this.getEntityTableName(),
938
+ forkBlockNum,
939
+ forkEventId
940
+ );
941
+ }
942
+ /**
943
+ * Story 4.4: архивировать entity_versions WHERE entity_table=... AND block_num > forkBlockNum
944
+ * в invalidated_entity_versions и удалить из entity_versions (атомарно). Возвращает count.
945
+ * Должен вызываться ПОСЛЕ restoreFromVersions — иначе restore не сможет прочитать ещё-живые версии.
946
+ */
947
+ async archiveInvalidatedVersionsSince(forkBlockNum, forkEventId) {
948
+ return this.entityVersioningService.archiveAndDeleteVersionsAfterFork(
949
+ this.getEntityTableName(),
950
+ forkBlockNum,
951
+ forkEventId
952
+ );
953
+ }
954
+ /**
955
+ * Обновить сущность
956
+ */
957
+ async update(entity) {
958
+ const typeormEntity = this.getMapper().toEntity(entity);
959
+ await this.entityVersioningService.saveVersionBeforeUpdate(
960
+ this.repository,
961
+ this.getEntityTableName(),
962
+ typeormEntity,
963
+ typeormEntity.block_num || null,
964
+ "update"
965
+ );
966
+ const savedEntity = await this.repository.save(typeormEntity);
967
+ return this.getMapper().toDomain(savedEntity);
968
+ }
969
+ /**
970
+ * Создание и валидация сущности без сохранения в базу данных
971
+ */
972
+ async create(entity) {
973
+ const typeormEntity = this.getMapper().toEntity(entity);
974
+ return this.repository.create(typeormEntity);
975
+ }
976
+ /**
977
+ * Сохранение созданной сущности в базу данных
978
+ */
979
+ async saveCreated(entity) {
980
+ const savedEntity = await this.repository.save(entity);
981
+ return this.getMapper().toDomain(savedEntity);
982
+ }
983
+ /**
984
+ * Сохранить сущность
985
+ */
986
+ async save(entity) {
987
+ const typeormEntity = this.getMapper().toEntity(entity);
988
+ if (typeormEntity._id) {
989
+ await this.entityVersioningService.saveVersionBeforeUpdate(
990
+ this.repository,
991
+ this.getEntityTableName(),
992
+ typeormEntity,
993
+ typeormEntity.block_num || null,
994
+ "save"
995
+ );
996
+ }
997
+ const savedEntity = await this.repository.save(typeormEntity);
998
+ return this.getMapper().toDomain(savedEntity);
999
+ }
1000
+ /**
1001
+ * Найти все сущности
1002
+ */
1003
+ async findAll() {
1004
+ const entities = await this.repository.find();
1005
+ return entities.map((entity) => this.getMapper().toDomain(entity));
1006
+ }
1007
+ /**
1008
+ * Найти сущность по внутреннему ID базы данных
1009
+ */
1010
+ async findById(_id) {
1011
+ const entity = await this.repository.findOne({
1012
+ where: { _id }
1013
+ });
1014
+ return entity ? this.getMapper().toDomain(entity) : null;
1015
+ }
1016
+ /**
1017
+ * Удалить сущность по внутреннему ID базы данных
1018
+ */
1019
+ async delete(_id) {
1020
+ await this.repository.delete(_id);
1021
+ }
1022
+ /**
1023
+ * Извлечь значение ключа синхронизации из блокчейн данных
1024
+ */
1025
+ extractSyncValueFromBlockchainData(blockchainData, syncKey) {
1026
+ const value = blockchainData[syncKey];
1027
+ if (value === null || value === void 0) {
1028
+ throw new Error(`Sync key '${syncKey}' not found in blockchain data`);
1029
+ }
1030
+ return value.toString().toLowerCase();
1031
+ }
1032
+ };
1033
+ exports.BaseBlockchainRepository = __decorateClass([
1034
+ common.Injectable()
1035
+ ], exports.BaseBlockchainRepository);
1036
+
1037
+ function auditUnknownStatus(entityName, receivedStatus, logger, allowedStatuses) {
1038
+ if (receivedStatus === void 0 || receivedStatus === null || receivedStatus === "") {
1039
+ return;
1040
+ }
1041
+ const expected = allowedStatuses && allowedStatuses.length > 0 ? `[${allowedStatuses.join(", ")}]` : "\u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u043E";
1042
+ logger.error(
1043
+ `UNKNOWN_ENTITY_STATUS ${entityName}: \u043F\u043E\u043B\u0443\u0447\u0435\u043D '${String(receivedStatus)}', \u043E\u0436\u0438\u0434\u0430\u044E\u0442\u0441\u044F ${expected}`,
1044
+ { entityName, receivedStatus, allowedStatuses }
1045
+ );
1046
+ }
1047
+
1048
+ const consoleAuditLogger = {
1049
+ error(message, ...meta) {
1050
+ console.error(message, ...meta);
1051
+ }
1052
+ };
1053
+ let currentAuditLogger = consoleAuditLogger;
1054
+ function configureAuditLogger(logger) {
1055
+ currentAuditLogger = logger;
1056
+ }
1057
+ function auditLogger() {
1058
+ return currentAuditLogger;
1059
+ }
1060
+
1061
+ exports.AbstractBlockchainDeltaMapper = AbstractBlockchainDeltaMapper;
1062
+ exports.BaseDomainEntity = BaseDomainEntity;
1063
+ exports.BaseTypeormEntity = BaseTypeormEntity;
1064
+ exports.FORK_AWARE_MARKER = FORK_AWARE_MARKER;
1065
+ exports.UnsupportedContractVersionError = UnsupportedContractVersionError;
1066
+ exports.auditLogger = auditLogger;
1067
+ exports.auditUnknownStatus = auditUnknownStatus;
1068
+ exports.configureAuditLogger = configureAuditLogger;
1069
+ exports.configureSyncPolicy = configureSyncPolicy;
1070
+ exports.isForkAware = isForkAware;
1071
+ exports.syncPolicy = syncPolicy;