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.
Files changed (310) hide show
  1. package/CHANGELOG.md +1111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +573 -0
  4. package/dist/audit/history-query.js +180 -0
  5. package/dist/audit/index.js +23 -0
  6. package/dist/audit/logger.js +236 -0
  7. package/dist/cache/cache-manager.js +84 -0
  8. package/dist/cache/index.js +16 -0
  9. package/dist/cache/redis-cluster-cache.js +557 -0
  10. package/dist/cli.js +2200 -0
  11. package/dist/compliance/audit-trail.js +68 -0
  12. package/dist/compliance/backup-verification.js +685 -0
  13. package/dist/compliance/breach-detector.js +505 -0
  14. package/dist/compliance/consent-record.js +227 -0
  15. package/dist/compliance/consent-versioning.js +331 -0
  16. package/dist/compliance/cross-border-log.js +530 -0
  17. package/dist/compliance/data-classifier.js +258 -0
  18. package/dist/compliance/data-lineage.js +565 -0
  19. package/dist/compliance/data-masker.js +303 -0
  20. package/dist/compliance/data-portability.js +265 -0
  21. package/dist/compliance/data-retention.js +195 -0
  22. package/dist/compliance/dsar-workflow.js +342 -0
  23. package/dist/compliance/field-encryption.js +223 -0
  24. package/dist/compliance/immutable-record.js +148 -0
  25. package/dist/compliance/index.js +260 -0
  26. package/dist/compliance/privacy-impact-assessment.js +364 -0
  27. package/dist/compliance/pseudonymization.js +269 -0
  28. package/dist/compliance/query-firewall.js +1182 -0
  29. package/dist/compliance/rate-limiter.js +580 -0
  30. package/dist/compliance/right-to-erasure.js +117 -0
  31. package/dist/compliance/row-level-security.js +246 -0
  32. package/dist/compliance/security-decorator.js +574 -0
  33. package/dist/compliance/security-monitor.js +525 -0
  34. package/dist/compliance/sensitive-data-discovery.js +476 -0
  35. package/dist/compliance/session-isolation.js +472 -0
  36. package/dist/compliance/tls-enforcer.js +108 -0
  37. package/dist/compliance/worm-storage.js +712 -0
  38. package/dist/connection-manager.js +198 -0
  39. package/dist/connection-pool.js +519 -0
  40. package/dist/decorators/audit.js +136 -0
  41. package/dist/decorators/belongs-to-many.js +115 -0
  42. package/dist/decorators/belongs-to.js +115 -0
  43. package/dist/decorators/check.js +435 -0
  44. package/dist/decorators/collate.js +329 -0
  45. package/dist/decorators/comment.js +205 -0
  46. package/dist/decorators/database-settings.js +236 -0
  47. package/dist/decorators/default.js +244 -0
  48. package/dist/decorators/encryption.js +235 -0
  49. package/dist/decorators/engine.js +97 -0
  50. package/dist/decorators/fk-constraints.js +594 -0
  51. package/dist/decorators/foreign-table.js +136 -0
  52. package/dist/decorators/generated.js +274 -0
  53. package/dist/decorators/has-many.js +127 -0
  54. package/dist/decorators/has-one.js +116 -0
  55. package/dist/decorators/hstore.js +129 -0
  56. package/dist/decorators/index.js +355 -0
  57. package/dist/decorators/json-column.js +83 -0
  58. package/dist/decorators/jsonb.js +101 -0
  59. package/dist/decorators/orm-decorators.js +425 -0
  60. package/dist/decorators/permissions.js +294 -0
  61. package/dist/decorators/procedure.js +210 -0
  62. package/dist/decorators/query-options.js +556 -0
  63. package/dist/decorators/range.js +167 -0
  64. package/dist/decorators/set-column.js +83 -0
  65. package/dist/decorators/spatial.js +114 -0
  66. package/dist/decorators/storage.js +580 -0
  67. package/dist/decorators/timezone.js +512 -0
  68. package/dist/decorators/trigger.js +90 -0
  69. package/dist/decorators/uuid.js +135 -0
  70. package/dist/decorators/view.js +258 -0
  71. package/dist/diagrams/chen-diagram.js +354 -0
  72. package/dist/diagrams/class-diagram.js +384 -0
  73. package/dist/diagrams/dependency-diagram.js +432 -0
  74. package/dist/diagrams/er-diagram.js +605 -0
  75. package/dist/diagrams/flow-diagram.js +394 -0
  76. package/dist/diagrams/gantt-diagram.js +411 -0
  77. package/dist/diagrams/index-diagram.js +353 -0
  78. package/dist/diagrams/index.js +184 -0
  79. package/dist/diagrams/migration-diagram.js +316 -0
  80. package/dist/diagrams/model-diagram.js +616 -0
  81. package/dist/diagrams/package-diagram.js +376 -0
  82. package/dist/diagrams/palette.js +85 -0
  83. package/dist/diagrams/relational-diagram.js +455 -0
  84. package/dist/diagrams/schemadoc-diagram.js +309 -0
  85. package/dist/diagrams/sequence-diagram.js +307 -0
  86. package/dist/diagrams/state-diagram.js +344 -0
  87. package/dist/diagrams/svg-dom.js +111 -0
  88. package/dist/diagrams/tree-diagram.js +250 -0
  89. package/dist/dialects/clickhouse/index.js +1742 -0
  90. package/dist/dialects/cockroachdb/index.js +4677 -0
  91. package/dist/dialects/cratedb/index.js +441 -0
  92. package/dist/dialects/databricks/index.js +517 -0
  93. package/dist/dialects/db2/index.js +2225 -0
  94. package/dist/dialects/dialect.js +720 -0
  95. package/dist/dialects/duckdb/index.js +2014 -0
  96. package/dist/dialects/exasol/index.js +357 -0
  97. package/dist/dialects/firebird/index.js +495 -0
  98. package/dist/dialects/greenplum/index.js +457 -0
  99. package/dist/dialects/hana/index.js +1741 -0
  100. package/dist/dialects/mariadb/index.js +3590 -0
  101. package/dist/dialects/mssql/index.js +2296 -0
  102. package/dist/dialects/mysql/index.js +4058 -0
  103. package/dist/dialects/oracle/index.js +2999 -0
  104. package/dist/dialects/postgres/index.js +5319 -0
  105. package/dist/dialects/query-stream-helper.js +152 -0
  106. package/dist/dialects/questdb/index.js +433 -0
  107. package/dist/dialects/redshift/index.js +2277 -0
  108. package/dist/dialects/singlestore/index.js +354 -0
  109. package/dist/dialects/snowflake/index.js +2082 -0
  110. package/dist/dialects/spanner/index.js +1768 -0
  111. package/dist/dialects/sqlite/index.js +3382 -0
  112. package/dist/dialects/tidb/index.js +377 -0
  113. package/dist/dialects/timescaledb/index.js +164 -0
  114. package/dist/dialects/trino/index.js +420 -0
  115. package/dist/dialects/turso/index.js +227 -0
  116. package/dist/dialects/vertica/index.js +317 -0
  117. package/dist/dialects/yugabytedb/index.js +424 -0
  118. package/dist/errors/index.js +472 -0
  119. package/dist/errors/utils.js +340 -0
  120. package/dist/errors.js +21 -0
  121. package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
  122. package/dist/extensions/catalog/mssql-features.js +147 -0
  123. package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
  124. package/dist/extensions/catalog/oracle-db2-features.js +196 -0
  125. package/dist/extensions/catalog/postgres-extensions.js +516 -0
  126. package/dist/extensions/index.js +71 -0
  127. package/dist/extensions/types.js +13 -0
  128. package/dist/foreign-data.js +173 -0
  129. package/dist/hooks/hooks-manager.js +350 -0
  130. package/dist/hooks/index.js +37 -0
  131. package/dist/index.js +463 -0
  132. package/dist/logging.js +335 -0
  133. package/dist/migrations/index.js +40 -0
  134. package/dist/migrations/migration.js +196 -0
  135. package/dist/migrations/migrator.js +411 -0
  136. package/dist/migrations/prormmigration.js +275 -0
  137. package/dist/migrations/query-interface.js +435 -0
  138. package/dist/migrations/seeder.js +353 -0
  139. package/dist/models/associations.js +852 -0
  140. package/dist/models/constraints.js +288 -0
  141. package/dist/models/data-types.js +2518 -0
  142. package/dist/models/decorators.js +445 -0
  143. package/dist/models/index.js +33 -0
  144. package/dist/models/indexes.js +531 -0
  145. package/dist/models/methods.js +382 -0
  146. package/dist/models/model-manager.js +103 -0
  147. package/dist/models/model.js +5349 -0
  148. package/dist/models/operators.js +67 -0
  149. package/dist/models/scopes.js +189 -0
  150. package/dist/models/typescript-types.js +26 -0
  151. package/dist/nosql/aerospike/index.js +205 -0
  152. package/dist/nosql/allegrograph/index.js +169 -0
  153. package/dist/nosql/arangodb/index.js +364 -0
  154. package/dist/nosql/azure-blob/index.js +206 -0
  155. package/dist/nosql/beanstalkd/index.js +231 -0
  156. package/dist/nosql/beequeue/index.js +210 -0
  157. package/dist/nosql/bigchaindb/index.js +195 -0
  158. package/dist/nosql/bigtable/index.js +224 -0
  159. package/dist/nosql/blazegraph/index.js +173 -0
  160. package/dist/nosql/bullmq/index.js +191 -0
  161. package/dist/nosql/cassandra/index.js +174 -0
  162. package/dist/nosql/chroma/index.js +190 -0
  163. package/dist/nosql/cloudflare-kv/index.js +220 -0
  164. package/dist/nosql/coherence/index.js +200 -0
  165. package/dist/nosql/cosmosdb/index.js +157 -0
  166. package/dist/nosql/couchbase/index.js +213 -0
  167. package/dist/nosql/dax/index.js +212 -0
  168. package/dist/nosql/deno-kv/index.js +206 -0
  169. package/dist/nosql/dgraph/index.js +171 -0
  170. package/dist/nosql/doris/index.js +169 -0
  171. package/dist/nosql/druid/index.js +162 -0
  172. package/dist/nosql/dynamodb/index.js +937 -0
  173. package/dist/nosql/elasticsearch/index.js +377 -0
  174. package/dist/nosql/etcd/index.js +502 -0
  175. package/dist/nosql/eventhubs/index.js +213 -0
  176. package/dist/nosql/eventstore/index.js +254 -0
  177. package/dist/nosql/faunadb/index.js +188 -0
  178. package/dist/nosql/firestore/index.js +177 -0
  179. package/dist/nosql/fluree/index.js +148 -0
  180. package/dist/nosql/fuseki/index.js +170 -0
  181. package/dist/nosql/gcs/index.js +172 -0
  182. package/dist/nosql/gearman/index.js +160 -0
  183. package/dist/nosql/geode/index.js +196 -0
  184. package/dist/nosql/graphdb/index.js +169 -0
  185. package/dist/nosql/graylog/index.js +188 -0
  186. package/dist/nosql/gridgain/index.js +171 -0
  187. package/dist/nosql/hazelcast/index.js +162 -0
  188. package/dist/nosql/hbase/index.js +230 -0
  189. package/dist/nosql/ignite/index.js +173 -0
  190. package/dist/nosql/immudb/index.js +184 -0
  191. package/dist/nosql/index.js +232 -0
  192. package/dist/nosql/infinispan/index.js +200 -0
  193. package/dist/nosql/influxdb/index.js +0 -0
  194. package/dist/nosql/kafka/index.js +234 -0
  195. package/dist/nosql/keyspaces/index.js +189 -0
  196. package/dist/nosql/kinesis/index.js +253 -0
  197. package/dist/nosql/leveldb/index.js +153 -0
  198. package/dist/nosql/lmdb/index.js +160 -0
  199. package/dist/nosql/loki/index.js +201 -0
  200. package/dist/nosql/marklogic/index.js +204 -0
  201. package/dist/nosql/materialize/index.js +145 -0
  202. package/dist/nosql/meilisearch/index.js +154 -0
  203. package/dist/nosql/memcached/index.js +223 -0
  204. package/dist/nosql/milvus/index.js +410 -0
  205. package/dist/nosql/minio/index.js +265 -0
  206. package/dist/nosql/momento/index.js +179 -0
  207. package/dist/nosql/mongodb/index.js +461 -0
  208. package/dist/nosql/nats/index.js +247 -0
  209. package/dist/nosql/nedb/index.js +164 -0
  210. package/dist/nosql/neo4j/index.js +450 -0
  211. package/dist/nosql/neptune/index.js +470 -0
  212. package/dist/nosql/nsq/index.js +200 -0
  213. package/dist/nosql/opensearch/index.js +186 -0
  214. package/dist/nosql/orientdb/index.js +175 -0
  215. package/dist/nosql/papertrail/index.js +200 -0
  216. package/dist/nosql/pinecone/index.js +0 -0
  217. package/dist/nosql/pinot/index.js +133 -0
  218. package/dist/nosql/pouchdb/index.js +172 -0
  219. package/dist/nosql/prometheus/index.js +174 -0
  220. package/dist/nosql/provendb/index.js +147 -0
  221. package/dist/nosql/pubsub/index.js +187 -0
  222. package/dist/nosql/pulsar/index.js +232 -0
  223. package/dist/nosql/qdrant/index.js +295 -0
  224. package/dist/nosql/qldb/index.js +192 -0
  225. package/dist/nosql/r2/index.js +281 -0
  226. package/dist/nosql/rabbitmq/index.js +237 -0
  227. package/dist/nosql/ravendb/index.js +175 -0
  228. package/dist/nosql/redis/index.js +607 -0
  229. package/dist/nosql/redpanda/index.js +237 -0
  230. package/dist/nosql/resque/index.js +203 -0
  231. package/dist/nosql/rethinkdb/index.js +232 -0
  232. package/dist/nosql/rocksdb/index.js +152 -0
  233. package/dist/nosql/rockset/index.js +126 -0
  234. package/dist/nosql/s3/index.js +298 -0
  235. package/dist/nosql/scylladb/index.js +178 -0
  236. package/dist/nosql/signoz/index.js +227 -0
  237. package/dist/nosql/sns/index.js +201 -0
  238. package/dist/nosql/solr/index.js +231 -0
  239. package/dist/nosql/splunk/index.js +227 -0
  240. package/dist/nosql/sqs/index.js +246 -0
  241. package/dist/nosql/stardog/index.js +169 -0
  242. package/dist/nosql/starrocks/index.js +170 -0
  243. package/dist/nosql/store.js +2 -0
  244. package/dist/nosql/sumologic/index.js +213 -0
  245. package/dist/nosql/surrealdb/index.js +179 -0
  246. package/dist/nosql/terminusdb/index.js +151 -0
  247. package/dist/nosql/tigergraph/index.js +469 -0
  248. package/dist/nosql/typesense/index.js +148 -0
  249. package/dist/nosql/unqlite/index.js +157 -0
  250. package/dist/nosql/upstash/index.js +167 -0
  251. package/dist/nosql/vercel-kv/index.js +166 -0
  252. package/dist/nosql/victoriametrics/index.js +213 -0
  253. package/dist/nosql/virtuoso/index.js +169 -0
  254. package/dist/nosql/weaviate/index.js +276 -0
  255. package/dist/operators/index.js +108 -0
  256. package/dist/operators.js +2690 -0
  257. package/dist/prisma-migrate/index.js +38 -0
  258. package/dist/prisma-migrate/migration-generator.js +125 -0
  259. package/dist/prisma-migrate/model-generator.js +172 -0
  260. package/dist/prisma-migrate/relations.js +100 -0
  261. package/dist/prisma-migrate/schema-parser.js +167 -0
  262. package/dist/prisma-migrate/type-mapper.js +40 -0
  263. package/dist/prorm.js +6832 -0
  264. package/dist/query-builders/cte-builder.js +80 -0
  265. package/dist/query-builders/functions/aggregate.js +390 -0
  266. package/dist/query-builders/functions/conditional.js +503 -0
  267. package/dist/query-builders/functions/datetime.js +695 -0
  268. package/dist/query-builders/functions/fulltext.js +439 -0
  269. package/dist/query-builders/functions/index.js +93 -0
  270. package/dist/query-builders/functions/json.js +427 -0
  271. package/dist/query-builders/functions/math.js +399 -0
  272. package/dist/query-builders/functions/string.js +518 -0
  273. package/dist/query-builders/functions/window.js +328 -0
  274. package/dist/query-builders/include-builder.js +323 -0
  275. package/dist/query-builders/index-expression-builder.js +242 -0
  276. package/dist/query-builders/index.js +161 -0
  277. package/dist/query-builders/insert-builder.js +164 -0
  278. package/dist/query-builders/model-helpers.js +38 -0
  279. package/dist/query-builders/order-limit-builder.js +239 -0
  280. package/dist/query-builders/sql-compiler.js +2040 -0
  281. package/dist/query-builders/subquery-builder.js +152 -0
  282. package/dist/query-builders/update-builder.js +182 -0
  283. package/dist/query-builders/view-builder.js +218 -0
  284. package/dist/query-builders/where-builder.js +1532 -0
  285. package/dist/query-interface.js +622 -0
  286. package/dist/query-optimizers/batch-optimizer.js +426 -0
  287. package/dist/query-optimizers/explain-plans.js +415 -0
  288. package/dist/query-optimizers/index.js +51 -0
  289. package/dist/query-optimizers/prepared-statement-cache.js +423 -0
  290. package/dist/query-optimizers/query-hints.js +438 -0
  291. package/dist/query-optimizers/query-optimizer.js +278 -0
  292. package/dist/query-optimizers/slow-query-logger.js +271 -0
  293. package/dist/replica-manager.js +507 -0
  294. package/dist/schema/index.js +15 -0
  295. package/dist/schema/migration-generator.js +374 -0
  296. package/dist/schema/schema-differ.js +549 -0
  297. package/dist/schema/types.js +6 -0
  298. package/dist/sql-constants.js +300 -0
  299. package/dist/sqlite-advanced.js +1047 -0
  300. package/dist/streams/index.js +12 -0
  301. package/dist/streams/transforms.js +178 -0
  302. package/dist/transaction.js +456 -0
  303. package/dist/types/index.js +180 -0
  304. package/dist/user-management.js +127 -0
  305. package/dist/utils/date.js +300 -0
  306. package/dist/utils/index.js +1079 -0
  307. package/dist/utils/string.js +161 -0
  308. package/dist/validators/index.js +19 -0
  309. package/dist/validators/validator.js +911 -0
  310. 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
+ }