ts-prorm-orm 1.1.0
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 +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WORMStorage (Write Once Read Many)
|
|
4
|
+
*
|
|
5
|
+
* Immutable archival storage that prevents UPDATE and DELETE operations after commit.
|
|
6
|
+
* Provides tamper-evident audit trails and retention period enforcement.
|
|
7
|
+
*
|
|
8
|
+
* Covers compliance frameworks including:
|
|
9
|
+
* SEC 17a-4 (WORM), FINRA, SOX, DORA, MiFID II, GDPR Art 32,
|
|
10
|
+
* ALCOA+, 21 CFR Part 11, and financial audit requirements.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* // Enable WORM on a model:
|
|
14
|
+
* WORMStorage.enable(FinancialRecord, {
|
|
15
|
+
* retentionPeriod: { years: 7 },
|
|
16
|
+
* tamperEvident: true,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Archive a record (moves to archived state):
|
|
20
|
+
* await WORMStorage.archive(record);
|
|
21
|
+
*
|
|
22
|
+
* // Freeze a record (fully immutable):
|
|
23
|
+
* await WORMStorage.freeze(record);
|
|
24
|
+
*
|
|
25
|
+
* // Verify integrity of a record:
|
|
26
|
+
* const isValid = await WORMStorage.verifyIntegrity(recordId, 'FinancialRecords');
|
|
27
|
+
*
|
|
28
|
+
* // Get tamper evidence log:
|
|
29
|
+
* const log = await WORMStorage.getTamperLog(recordId, 'FinancialRecords');
|
|
30
|
+
*
|
|
31
|
+
* // Enforce retention policy:
|
|
32
|
+
* await WORMStorage.enforceRetention(FinancialRecord, prorm);
|
|
33
|
+
*
|
|
34
|
+
* // Using the decorator:
|
|
35
|
+
* @WORM({ retentionPeriod: { years: 7 }, tamperEvident: true })
|
|
36
|
+
* class FinancialRecord extends Model { ... }
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.WORMStorage = void 0;
|
|
40
|
+
exports.WORM = WORM;
|
|
41
|
+
exports.getWORMOptions = getWORMOptions;
|
|
42
|
+
exports.applyWORMHooks = applyWORMHooks;
|
|
43
|
+
exports.isFrozen = isFrozen;
|
|
44
|
+
exports.isArchived = isArchived;
|
|
45
|
+
const crypto_1 = require("crypto");
|
|
46
|
+
const data_types_1 = require("../models/data-types");
|
|
47
|
+
const data_retention_1 = require("./data-retention");
|
|
48
|
+
// ==================== Symbol Keys ====================
|
|
49
|
+
const WORM_OPTIONS_KEY = '__orm_worm_options__';
|
|
50
|
+
// ==================== Registry ====================
|
|
51
|
+
const wormRegistry = new Map();
|
|
52
|
+
// ==================== Hash Utilities ====================
|
|
53
|
+
/**
|
|
54
|
+
* Cryptographic hash for tamper detection.
|
|
55
|
+
* Uses SHA-256 (via Node's built-in crypto module) over a canonical
|
|
56
|
+
* (sorted-key) JSON representation of the record data plus the previous
|
|
57
|
+
* hash in the chain, so the result is deterministic regardless of key
|
|
58
|
+
* insertion order and each entry is cryptographically bound to its
|
|
59
|
+
* predecessor.
|
|
60
|
+
*/
|
|
61
|
+
function computeHash(data, previousHash = '') {
|
|
62
|
+
const content = stableStringify(data) + previousHash;
|
|
63
|
+
return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Deterministically stringify an object by sorting object keys at every
|
|
67
|
+
* level, so semantically identical records always hash to the same value.
|
|
68
|
+
*/
|
|
69
|
+
function stableStringify(value) {
|
|
70
|
+
if (value === null || typeof value !== 'object') {
|
|
71
|
+
return JSON.stringify(value);
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
return `[${value.map((v) => stableStringify(v)).join(',')}]`;
|
|
75
|
+
}
|
|
76
|
+
if (value instanceof Date) {
|
|
77
|
+
return JSON.stringify(value.toISOString());
|
|
78
|
+
}
|
|
79
|
+
const keys = Object.keys(value).sort();
|
|
80
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
|
|
81
|
+
}
|
|
82
|
+
// ==================== Model Creation ====================
|
|
83
|
+
function createWORMMetadataModel(prorm, tableName, stateField) {
|
|
84
|
+
const MetadataModel = prorm.define('WORMMetadata', {
|
|
85
|
+
id: {
|
|
86
|
+
type: data_types_1.DataTypes.INTEGER(),
|
|
87
|
+
primaryKey: true,
|
|
88
|
+
autoIncrement: true,
|
|
89
|
+
},
|
|
90
|
+
recordId: {
|
|
91
|
+
type: data_types_1.DataTypes.STRING(),
|
|
92
|
+
allowNull: false,
|
|
93
|
+
},
|
|
94
|
+
tableName: {
|
|
95
|
+
type: data_types_1.DataTypes.STRING(),
|
|
96
|
+
allowNull: false,
|
|
97
|
+
},
|
|
98
|
+
state: {
|
|
99
|
+
type: data_types_1.DataTypes.ENUM('active', 'archived', 'frozen'),
|
|
100
|
+
allowNull: false,
|
|
101
|
+
defaultValue: 'active',
|
|
102
|
+
},
|
|
103
|
+
archivedAt: {
|
|
104
|
+
type: data_types_1.DataTypes.DATE,
|
|
105
|
+
allowNull: true,
|
|
106
|
+
},
|
|
107
|
+
frozenAt: {
|
|
108
|
+
type: data_types_1.DataTypes.DATE,
|
|
109
|
+
allowNull: true,
|
|
110
|
+
},
|
|
111
|
+
retentionUntil: {
|
|
112
|
+
type: data_types_1.DataTypes.DATE,
|
|
113
|
+
allowNull: true,
|
|
114
|
+
},
|
|
115
|
+
}, {
|
|
116
|
+
tableName,
|
|
117
|
+
timestamps: true,
|
|
118
|
+
indexes: [
|
|
119
|
+
{ fields: ['recordId', 'tableName'] },
|
|
120
|
+
{ fields: ['state'] },
|
|
121
|
+
{ fields: ['retentionUntil'] },
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
return MetadataModel;
|
|
125
|
+
}
|
|
126
|
+
function createWORMTamperLogModel(prorm, tableName) {
|
|
127
|
+
const TamperLogModel = prorm.define('WORMTamperLog', {
|
|
128
|
+
id: {
|
|
129
|
+
type: data_types_1.DataTypes.INTEGER(),
|
|
130
|
+
primaryKey: true,
|
|
131
|
+
autoIncrement: true,
|
|
132
|
+
},
|
|
133
|
+
recordId: {
|
|
134
|
+
type: data_types_1.DataTypes.STRING(),
|
|
135
|
+
allowNull: false,
|
|
136
|
+
},
|
|
137
|
+
tableName: {
|
|
138
|
+
type: data_types_1.DataTypes.STRING(),
|
|
139
|
+
allowNull: false,
|
|
140
|
+
},
|
|
141
|
+
operation: {
|
|
142
|
+
type: data_types_1.DataTypes.ENUM('create', 'archive', 'freeze', 'unfreeze', 'tamper_detected'),
|
|
143
|
+
allowNull: false,
|
|
144
|
+
},
|
|
145
|
+
hash: {
|
|
146
|
+
type: data_types_1.DataTypes.STRING(64),
|
|
147
|
+
allowNull: false,
|
|
148
|
+
},
|
|
149
|
+
previousHash: {
|
|
150
|
+
type: data_types_1.DataTypes.STRING(64),
|
|
151
|
+
allowNull: true,
|
|
152
|
+
},
|
|
153
|
+
performedBy: {
|
|
154
|
+
type: data_types_1.DataTypes.STRING(),
|
|
155
|
+
allowNull: true,
|
|
156
|
+
},
|
|
157
|
+
metadata: {
|
|
158
|
+
type: data_types_1.DataTypes.JSON(),
|
|
159
|
+
allowNull: true,
|
|
160
|
+
},
|
|
161
|
+
}, {
|
|
162
|
+
tableName: `${tableName}_tamper_logs`,
|
|
163
|
+
timestamps: true,
|
|
164
|
+
indexes: [
|
|
165
|
+
{ fields: ['recordId', 'tableName'] },
|
|
166
|
+
{ fields: ['hash'] },
|
|
167
|
+
{ fields: ['timestamp'] },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
return TamperLogModel;
|
|
171
|
+
}
|
|
172
|
+
// ==================== Decorator ====================
|
|
173
|
+
/**
|
|
174
|
+
* Decorator to mark a model for WORM storage.
|
|
175
|
+
*
|
|
176
|
+
* @WORM({ retentionPeriod: { years: 7 }, tamperEvident: true })
|
|
177
|
+
* class FinancialRecord extends Model { ... }
|
|
178
|
+
*/
|
|
179
|
+
function WORM(options) {
|
|
180
|
+
return function (target) {
|
|
181
|
+
target[WORM_OPTIONS_KEY] = options;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get the WORM options for a model.
|
|
186
|
+
*/
|
|
187
|
+
function getWORMOptions(Model) {
|
|
188
|
+
return Model[WORM_OPTIONS_KEY];
|
|
189
|
+
}
|
|
190
|
+
// ==================== WORMStorage Class ====================
|
|
191
|
+
class WORMStorage {
|
|
192
|
+
/**
|
|
193
|
+
* Enable WORM storage on a model.
|
|
194
|
+
* Creates necessary metadata tables and registers hooks.
|
|
195
|
+
*
|
|
196
|
+
* @param model - The model class to protect
|
|
197
|
+
* @param prorm - The Prorm instance
|
|
198
|
+
* @param options - WORM configuration options
|
|
199
|
+
*/
|
|
200
|
+
static enable(model, prorm, options) {
|
|
201
|
+
const metadataTableName = options.tableName ?? 'WORMMetadata';
|
|
202
|
+
const stateField = options.stateField ?? 'wormState';
|
|
203
|
+
const timestampField = options.timestampField ?? 'archivedAt';
|
|
204
|
+
const metadataModel = createWORMMetadataModel(prorm, metadataTableName, stateField);
|
|
205
|
+
const tamperLogModel = createWORMTamperLogModel(prorm, metadataTableName);
|
|
206
|
+
const config = {
|
|
207
|
+
model,
|
|
208
|
+
prorm,
|
|
209
|
+
options: {
|
|
210
|
+
retentionPeriod: options.retentionPeriod,
|
|
211
|
+
tamperEvident: options.tamperEvident ?? true,
|
|
212
|
+
tableName: metadataTableName,
|
|
213
|
+
stateField,
|
|
214
|
+
timestampField,
|
|
215
|
+
allowSoftDelete: options.allowSoftDelete ?? false,
|
|
216
|
+
autoArchive: options.autoArchive ?? false,
|
|
217
|
+
autoFreeze: options.autoFreeze ?? false,
|
|
218
|
+
integrateWithRetention: options.integrateWithRetention ?? true,
|
|
219
|
+
},
|
|
220
|
+
metadataModel,
|
|
221
|
+
tamperLogModel,
|
|
222
|
+
};
|
|
223
|
+
wormRegistry.set(model, config);
|
|
224
|
+
// Apply hooks
|
|
225
|
+
this.applyHooks(model, config);
|
|
226
|
+
// Integrate with DataRetentionPolicy if enabled
|
|
227
|
+
if (config.options.integrateWithRetention) {
|
|
228
|
+
data_retention_1.DataRetentionPolicy.set(model, {
|
|
229
|
+
maxAge: config.options.retentionPeriod,
|
|
230
|
+
field: timestampField,
|
|
231
|
+
onExpiry: 'delete',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Apply hooks to a model for WORM protection.
|
|
237
|
+
* Called automatically by enable().
|
|
238
|
+
*
|
|
239
|
+
* @param model - The model class
|
|
240
|
+
* @param config - The WORM configuration
|
|
241
|
+
*/
|
|
242
|
+
static applyHooks(model, config) {
|
|
243
|
+
const cfg = config ?? wormRegistry.get(model);
|
|
244
|
+
if (!cfg) {
|
|
245
|
+
throw new Error(`[WORMStorage] No WORM configuration found for model "${model.name ?? model}". ` +
|
|
246
|
+
`Call WORMStorage.enable(Model, prorm, options) first.`);
|
|
247
|
+
}
|
|
248
|
+
const { metadataModel, tamperLogModel, options } = cfg;
|
|
249
|
+
const hookName = 'orm:wormStorage';
|
|
250
|
+
// beforeCreate hook - initialize WORM metadata
|
|
251
|
+
model.addHook('beforeCreate', hookName, async function (record) {
|
|
252
|
+
const tableName = model.tableName ?? model.name;
|
|
253
|
+
// Create metadata entry
|
|
254
|
+
await metadataModel.create({
|
|
255
|
+
recordId: record.id,
|
|
256
|
+
tableName,
|
|
257
|
+
state: 'active',
|
|
258
|
+
});
|
|
259
|
+
// Record tamper log if enabled
|
|
260
|
+
if (options.tamperEvident) {
|
|
261
|
+
const recordData = record.toJSON ? record.toJSON() : record.dataValues;
|
|
262
|
+
const hash = computeHash(recordData);
|
|
263
|
+
await tamperLogModel.create({
|
|
264
|
+
recordId: record.id,
|
|
265
|
+
tableName,
|
|
266
|
+
operation: 'create',
|
|
267
|
+
hash,
|
|
268
|
+
previousHash: '',
|
|
269
|
+
metadata: { initialState: true },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// Auto-archive if enabled
|
|
273
|
+
if (options.autoArchive) {
|
|
274
|
+
const retentionUntil = data_retention_1.DataRetentionPolicy.cutoffDate(options.retentionPeriod);
|
|
275
|
+
await metadataModel.update({
|
|
276
|
+
state: 'archived',
|
|
277
|
+
archivedAt: new Date(),
|
|
278
|
+
retentionUntil,
|
|
279
|
+
}, { where: { recordId: record.id, tableName } });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
// beforeUpdate hook - block updates on archived/frozen records
|
|
283
|
+
model.addHook('beforeUpdate', hookName, async function (record) {
|
|
284
|
+
const tableName = model.tableName ?? model.name;
|
|
285
|
+
const metadata = await metadataModel.findOne({
|
|
286
|
+
where: { recordId: record.id, tableName },
|
|
287
|
+
});
|
|
288
|
+
if (metadata) {
|
|
289
|
+
const state = metadata.state;
|
|
290
|
+
if (state === 'frozen') {
|
|
291
|
+
throw new Error(`[WORMStorage] UPDATE is not allowed on frozen record "${record.id}" in table "${tableName}". ` +
|
|
292
|
+
`This record is immutable for SEC 17a-4 compliance.`);
|
|
293
|
+
}
|
|
294
|
+
if (state === 'archived') {
|
|
295
|
+
// Check if this is an archive operation
|
|
296
|
+
const changedFields = Object.keys(record._changed ?? {});
|
|
297
|
+
if (changedFields.length > 0 && !changedFields.includes(options.stateField)) {
|
|
298
|
+
throw new Error(`[WORMStorage] UPDATE is not allowed on archived record "${record.id}" in table "${tableName}". ` +
|
|
299
|
+
`Archive records are immutable. Use WORMStorage.freeze() to explicitly freeze, ` +
|
|
300
|
+
`or create a new record instead.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Verify tamper integrity if enabled
|
|
304
|
+
if (options.tamperEvident) {
|
|
305
|
+
const isValid = await WORMStorage.verifyIntegrity(record.id, tableName);
|
|
306
|
+
if (!isValid) {
|
|
307
|
+
throw new Error(`[WORMStorage] TAMPER DETECTED on record "${record.id}" in table "${tableName}". ` +
|
|
308
|
+
`The record hash does not match the chain.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
// beforeDestroy hook - block deletes on archived/frozen records
|
|
314
|
+
model.addHook('beforeDestroy', hookName, async function (record, options) {
|
|
315
|
+
const tableName = model.tableName ?? model.name;
|
|
316
|
+
const metadata = await metadataModel.findOne({
|
|
317
|
+
where: { recordId: record.id, tableName },
|
|
318
|
+
});
|
|
319
|
+
if (metadata) {
|
|
320
|
+
const state = metadata.state;
|
|
321
|
+
if (options.force) {
|
|
322
|
+
// Allow hard delete only if not archived/frozen
|
|
323
|
+
if (state === 'frozen' || state === 'archived') {
|
|
324
|
+
throw new Error(`[WORMStorage] DELETE is not allowed on ${state} record "${record.id}" in table "${tableName}". ` +
|
|
325
|
+
`WORM storage prevents deletion of archived/frozen records for SEC 17a-4 compliance.`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (!options.force) {
|
|
329
|
+
// Soft delete behavior
|
|
330
|
+
if (options.allowSoftDelete) {
|
|
331
|
+
// Allow soft delete but log it
|
|
332
|
+
if (options.tamperEvident) {
|
|
333
|
+
await tamperLogModel.create({
|
|
334
|
+
recordId: record.id,
|
|
335
|
+
tableName,
|
|
336
|
+
operation: 'tamper_detected',
|
|
337
|
+
hash: 'soft_delete_attempt',
|
|
338
|
+
metadata: { reason: 'soft_delete_blocked', state },
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
throw new Error(`[WORMStorage] DELETE is not allowed on record "${record.id}" in table "${tableName}". ` +
|
|
344
|
+
`This record is protected by WORM storage for compliance.`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
// afterUpdate hook - track state changes
|
|
350
|
+
model.addHook('afterUpdate', hookName, async function (record) {
|
|
351
|
+
if (options.tamperEvident) {
|
|
352
|
+
const tableName = model.tableName ?? model.name;
|
|
353
|
+
const metadata = await metadataModel.findOne({
|
|
354
|
+
where: { recordId: record.id, tableName },
|
|
355
|
+
});
|
|
356
|
+
if (metadata) {
|
|
357
|
+
const previousData = record.previous();
|
|
358
|
+
const currentData = record.toJSON ? record.toJSON() : record.dataValues;
|
|
359
|
+
// Get last hash
|
|
360
|
+
const lastLog = await tamperLogModel.findOne({
|
|
361
|
+
where: { recordId: record.id, tableName },
|
|
362
|
+
order: [['createdAt', 'DESC']],
|
|
363
|
+
});
|
|
364
|
+
const hash = computeHash(currentData, lastLog?.hash ?? '');
|
|
365
|
+
await tamperLogModel.create({
|
|
366
|
+
recordId: record.id,
|
|
367
|
+
tableName,
|
|
368
|
+
operation: 'create', // Treat update as new entry in chain
|
|
369
|
+
hash,
|
|
370
|
+
previousHash: lastLog?.hash ?? '',
|
|
371
|
+
metadata: { previousState: previousData, currentState: currentData },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Archive a record (moves to archived state).
|
|
379
|
+
* Archived records are read-only but can still be queried.
|
|
380
|
+
*
|
|
381
|
+
* @param record - The record instance to archive
|
|
382
|
+
*/
|
|
383
|
+
static async archive(record) {
|
|
384
|
+
const model = record.constructor;
|
|
385
|
+
const config = wormRegistry.get(model);
|
|
386
|
+
if (!config) {
|
|
387
|
+
throw new Error(`[WORMStorage] No WORM configuration found for model "${model.name ?? model}". ` +
|
|
388
|
+
`Call WORMStorage.enable(Model, prorm, options) first.`);
|
|
389
|
+
}
|
|
390
|
+
const { metadataModel, tamperLogModel, options } = config;
|
|
391
|
+
const tableName = model.tableName ?? model.name;
|
|
392
|
+
const recordId = record.id;
|
|
393
|
+
// Get current state
|
|
394
|
+
const metadata = await metadataModel.findOne({
|
|
395
|
+
where: { recordId, tableName },
|
|
396
|
+
});
|
|
397
|
+
if (!metadata) {
|
|
398
|
+
throw new Error(`[WORMStorage] No WORM metadata found for record "${recordId}".`);
|
|
399
|
+
}
|
|
400
|
+
if (metadata.state === 'frozen') {
|
|
401
|
+
throw new Error(`[WORMStorage] Cannot archive frozen record "${recordId}". Frozen records cannot be modified.`);
|
|
402
|
+
}
|
|
403
|
+
if (metadata.state === 'archived') {
|
|
404
|
+
// Already archived, just return
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Calculate retention until date
|
|
408
|
+
const retentionUntil = data_retention_1.DataRetentionPolicy.cutoffDate(options.retentionPeriod);
|
|
409
|
+
// Update state to archived
|
|
410
|
+
await metadataModel.update({
|
|
411
|
+
state: 'archived',
|
|
412
|
+
archivedAt: new Date(),
|
|
413
|
+
retentionUntil,
|
|
414
|
+
}, { where: { recordId, tableName } });
|
|
415
|
+
// Record tamper log
|
|
416
|
+
if (options.tamperEvident) {
|
|
417
|
+
const recordData = record.toJSON ? record.toJSON() : record.dataValues;
|
|
418
|
+
const lastLog = await tamperLogModel.findOne({
|
|
419
|
+
where: { recordId, tableName },
|
|
420
|
+
order: [['createdAt', 'DESC']],
|
|
421
|
+
});
|
|
422
|
+
// Chain this entry's hash off the previous entry so the hash itself
|
|
423
|
+
// (not just the previousHash pointer) is bound to the prior chain
|
|
424
|
+
// state - required for verifyIntegrity() to detect tampering.
|
|
425
|
+
const hash = computeHash(recordData, lastLog?.hash ?? '');
|
|
426
|
+
await tamperLogModel.create({
|
|
427
|
+
recordId,
|
|
428
|
+
tableName,
|
|
429
|
+
operation: 'archive',
|
|
430
|
+
hash,
|
|
431
|
+
previousHash: lastLog?.hash ?? '',
|
|
432
|
+
metadata: { archivedState: true, retentionUntil },
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Freeze a record (fully immutable).
|
|
438
|
+
* Frozen records cannot be modified or deleted.
|
|
439
|
+
*
|
|
440
|
+
* @param record - The record instance to freeze
|
|
441
|
+
*/
|
|
442
|
+
static async freeze(record) {
|
|
443
|
+
const model = record.constructor;
|
|
444
|
+
const config = wormRegistry.get(model);
|
|
445
|
+
if (!config) {
|
|
446
|
+
throw new Error(`[WORMStorage] No WORM configuration found for model "${model.name ?? model}". ` +
|
|
447
|
+
`Call WORMStorage.enable(Model, prorm, options) first.`);
|
|
448
|
+
}
|
|
449
|
+
const { metadataModel, tamperLogModel, options } = config;
|
|
450
|
+
const tableName = model.tableName ?? model.name;
|
|
451
|
+
const recordId = record.id;
|
|
452
|
+
// Get current state
|
|
453
|
+
const metadata = await metadataModel.findOne({
|
|
454
|
+
where: { recordId, tableName },
|
|
455
|
+
});
|
|
456
|
+
if (!metadata) {
|
|
457
|
+
throw new Error(`[WORMStorage] No WORM metadata found for record "${recordId}".`);
|
|
458
|
+
}
|
|
459
|
+
if (metadata.state === 'frozen') {
|
|
460
|
+
// Already frozen, just return
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// Update state to frozen
|
|
464
|
+
await metadataModel.update({
|
|
465
|
+
state: 'frozen',
|
|
466
|
+
frozenAt: new Date(),
|
|
467
|
+
}, { where: { recordId, tableName } });
|
|
468
|
+
// Record tamper log
|
|
469
|
+
if (options.tamperEvident) {
|
|
470
|
+
const recordData = record.toJSON ? record.toJSON() : record.dataValues;
|
|
471
|
+
const lastLog = await tamperLogModel.findOne({
|
|
472
|
+
where: { recordId, tableName },
|
|
473
|
+
order: [['createdAt', 'DESC']],
|
|
474
|
+
});
|
|
475
|
+
// Chain this entry's hash off the previous entry (see archive() for
|
|
476
|
+
// rationale).
|
|
477
|
+
const hash = computeHash(recordData, lastLog?.hash ?? '');
|
|
478
|
+
await tamperLogModel.create({
|
|
479
|
+
recordId,
|
|
480
|
+
tableName,
|
|
481
|
+
operation: 'freeze',
|
|
482
|
+
hash,
|
|
483
|
+
previousHash: lastLog?.hash ?? '',
|
|
484
|
+
metadata: { frozenState: true },
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get the tamper evidence log for a record.
|
|
490
|
+
*
|
|
491
|
+
* @param recordId - The record ID
|
|
492
|
+
* @param tableName - The table name
|
|
493
|
+
*/
|
|
494
|
+
static async getTamperLog(recordId, tableName) {
|
|
495
|
+
// Find the config for this table
|
|
496
|
+
let config;
|
|
497
|
+
for (const [, cfg] of wormRegistry) {
|
|
498
|
+
if ((cfg.model.tableName ?? cfg.model.name) === tableName) {
|
|
499
|
+
config = cfg;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!config) {
|
|
504
|
+
throw new Error(`[WORMStorage] No WORM configuration found for table "${tableName}". ` +
|
|
505
|
+
`Call WORMStorage.enable(Model, prorm, options) first.`);
|
|
506
|
+
}
|
|
507
|
+
const { tamperLogModel } = config;
|
|
508
|
+
const logs = await tamperLogModel.findAll({
|
|
509
|
+
where: { recordId: String(recordId), tableName },
|
|
510
|
+
order: [['createdAt', 'ASC']],
|
|
511
|
+
});
|
|
512
|
+
return logs.map((log) => log.toJSON());
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Verify the integrity of a record's tamper chain.
|
|
516
|
+
*
|
|
517
|
+
* @param recordId - The record ID
|
|
518
|
+
* @param tableName - The table name
|
|
519
|
+
* @returns True if chain is valid, false if tampering detected
|
|
520
|
+
*/
|
|
521
|
+
static async verifyIntegrity(recordId, tableName) {
|
|
522
|
+
// Find the config for this table
|
|
523
|
+
let config;
|
|
524
|
+
for (const [, cfg] of wormRegistry) {
|
|
525
|
+
if ((cfg.model.tableName ?? cfg.model.name) === tableName) {
|
|
526
|
+
config = cfg;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!config) {
|
|
531
|
+
throw new Error(`[WORMStorage] No WORM configuration found for table "${tableName}". ` +
|
|
532
|
+
`Call WORMStorage.enable(Model, prorm, options) first.`);
|
|
533
|
+
}
|
|
534
|
+
const { tamperLogModel, model } = config;
|
|
535
|
+
// Get all tamper log entries
|
|
536
|
+
const allLogs = await tamperLogModel.findAll({
|
|
537
|
+
where: { recordId: String(recordId), tableName },
|
|
538
|
+
order: [['createdAt', 'ASC']],
|
|
539
|
+
});
|
|
540
|
+
if (allLogs.length === 0) {
|
|
541
|
+
// No logs yet, check if record exists
|
|
542
|
+
const record = await model.findByPk(recordId);
|
|
543
|
+
return !!record;
|
|
544
|
+
}
|
|
545
|
+
// 'tamper_detected' entries (e.g. blocked soft-delete attempts) are
|
|
546
|
+
// flags, not members of the create/archive/freeze/update hash chain -
|
|
547
|
+
// they don't carry a real previousHash, so exclude them from chain
|
|
548
|
+
// verification.
|
|
549
|
+
const logs = allLogs.filter((log) => log.operation !== 'tamper_detected');
|
|
550
|
+
if (logs.length === 0) {
|
|
551
|
+
const record = await model.findByPk(recordId);
|
|
552
|
+
return !!record;
|
|
553
|
+
}
|
|
554
|
+
// Verify hash chain linkage: each entry's previousHash must match the
|
|
555
|
+
// hash recorded by the entry immediately before it.
|
|
556
|
+
for (let i = 0; i < logs.length; i++) {
|
|
557
|
+
const log = logs[i];
|
|
558
|
+
const expectedPreviousHash = i > 0 ? logs[i - 1].hash : '';
|
|
559
|
+
if ((log.previousHash ?? '') !== expectedPreviousHash) {
|
|
560
|
+
// Chain is broken - tampering detected
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Get the actual record and verify its current data still matches the
|
|
565
|
+
// hash recorded for the most recent chain entry. This is what makes
|
|
566
|
+
// the check tamper-*evident*: if the record's persisted data has been
|
|
567
|
+
// altered outside of the WORM hooks (e.g. a direct DB edit bypassing
|
|
568
|
+
// this ORM), or if the stored hash itself was edited, the recomputed
|
|
569
|
+
// hash will no longer match and this returns false.
|
|
570
|
+
const record = await model.findByPk(recordId);
|
|
571
|
+
if (!record) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
const recordData = record.toJSON ? record.toJSON() : record.dataValues;
|
|
575
|
+
const lastLog = logs[logs.length - 1];
|
|
576
|
+
const recomputedHash = computeHash(recordData, lastLog.previousHash ?? '');
|
|
577
|
+
return recomputedHash === lastLog.hash;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Check if WORM is enabled on a model.
|
|
581
|
+
*
|
|
582
|
+
* @param model - The model class
|
|
583
|
+
*/
|
|
584
|
+
static isWORMEnabled(model) {
|
|
585
|
+
return wormRegistry.has(model);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Get the WORM configuration for a model.
|
|
589
|
+
*
|
|
590
|
+
* @param model - The model class
|
|
591
|
+
*/
|
|
592
|
+
static getConfig(model) {
|
|
593
|
+
const config = wormRegistry.get(model);
|
|
594
|
+
return config?.options;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Enforce retention policy on a model.
|
|
598
|
+
* This will delete or anonymize records that have exceeded their retention period.
|
|
599
|
+
*
|
|
600
|
+
* @param model - The model class
|
|
601
|
+
* @param prorm - The Prorm instance
|
|
602
|
+
*/
|
|
603
|
+
static async enforceRetention(model, prorm) {
|
|
604
|
+
const config = wormRegistry.get(model);
|
|
605
|
+
if (!config) {
|
|
606
|
+
throw new Error(`[WORMStorage] No WORM configuration found for model "${model.name ?? model}". ` +
|
|
607
|
+
`Call WORMStorage.enable(Model, prorm, options) first.`);
|
|
608
|
+
}
|
|
609
|
+
// Use DataRetentionPolicy to enforce retention
|
|
610
|
+
return data_retention_1.DataRetentionPolicy.purge(model, prorm);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get the archive state of a record.
|
|
614
|
+
*
|
|
615
|
+
* @param recordId - The record ID
|
|
616
|
+
* @param tableName - The table name
|
|
617
|
+
*/
|
|
618
|
+
static async getState(recordId, tableName) {
|
|
619
|
+
// Find the config for this table
|
|
620
|
+
let config;
|
|
621
|
+
for (const [, cfg] of wormRegistry) {
|
|
622
|
+
if ((cfg.model.tableName ?? cfg.model.name) === tableName) {
|
|
623
|
+
config = cfg;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (!config) {
|
|
628
|
+
throw new Error(`[WORMStorage] No WORM configuration found for table "${tableName}". ` +
|
|
629
|
+
`Call WORMStorage.enable(Model, prorm, options) first.`);
|
|
630
|
+
}
|
|
631
|
+
const { metadataModel } = config;
|
|
632
|
+
const metadata = await metadataModel.findOne({
|
|
633
|
+
where: { recordId: String(recordId), tableName },
|
|
634
|
+
});
|
|
635
|
+
return metadata ? metadata.state : null;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Disable WORM storage for a model.
|
|
639
|
+
*
|
|
640
|
+
* @param model - The model class
|
|
641
|
+
*/
|
|
642
|
+
static disable(model) {
|
|
643
|
+
const config = wormRegistry.get(model);
|
|
644
|
+
if (config) {
|
|
645
|
+
// Remove hooks
|
|
646
|
+
const hookName = 'orm:wormStorage';
|
|
647
|
+
if (typeof config.model.removeHook === 'function') {
|
|
648
|
+
config.model.removeHook(hookName);
|
|
649
|
+
}
|
|
650
|
+
// Remove from DataRetentionPolicy if integrated
|
|
651
|
+
if (config.options.integrateWithRetention) {
|
|
652
|
+
data_retention_1.DataRetentionPolicy.unset(model);
|
|
653
|
+
}
|
|
654
|
+
wormRegistry.delete(model);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Get the WORM metadata model for a model.
|
|
659
|
+
*
|
|
660
|
+
* @param model - The model class
|
|
661
|
+
*/
|
|
662
|
+
static getMetadataModel(model) {
|
|
663
|
+
return wormRegistry.get(model)?.metadataModel;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get the tamper log model for a model.
|
|
667
|
+
*
|
|
668
|
+
* @param model - The model class
|
|
669
|
+
*/
|
|
670
|
+
static getTamperLogModel(model) {
|
|
671
|
+
return wormRegistry.get(model)?.tamperLogModel;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* List all models with WORM storage enabled.
|
|
675
|
+
*/
|
|
676
|
+
static listWORMModels() {
|
|
677
|
+
return Array.from(wormRegistry.entries()).map(([model, config]) => ({
|
|
678
|
+
modelName: model.name ?? String(model),
|
|
679
|
+
options: config.options,
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
exports.WORMStorage = WORMStorage;
|
|
684
|
+
// ==================== Utility Functions ====================
|
|
685
|
+
/**
|
|
686
|
+
* Apply WORM hooks to a model (exported for convenience).
|
|
687
|
+
*
|
|
688
|
+
* @param model - The model class
|
|
689
|
+
*/
|
|
690
|
+
function applyWORMHooks(model) {
|
|
691
|
+
WORMStorage.applyHooks(model);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Check if a specific record is frozen.
|
|
695
|
+
*
|
|
696
|
+
* @param recordId - The record ID
|
|
697
|
+
* @param tableName - The table name
|
|
698
|
+
*/
|
|
699
|
+
async function isFrozen(recordId, tableName) {
|
|
700
|
+
const state = await WORMStorage.getState(recordId, tableName);
|
|
701
|
+
return state === 'frozen';
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Check if a specific record is archived.
|
|
705
|
+
*
|
|
706
|
+
* @param recordId - The record ID
|
|
707
|
+
* @param tableName - The table name
|
|
708
|
+
*/
|
|
709
|
+
async function isArchived(recordId, tableName) {
|
|
710
|
+
const state = await WORMStorage.getState(recordId, tableName);
|
|
711
|
+
return state === 'archived';
|
|
712
|
+
}
|