@cqrs-ddd/mikro-orm 0.2.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/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +299 -0
- package/dist/cache/cache-entry.d.ts +43 -0
- package/dist/cache/cache-entry.d.ts.map +1 -0
- package/dist/cache/cache-entry.js +92 -0
- package/dist/cache/cache-entry.js.map +1 -0
- package/dist/cache/mikro-orm.cache.d.ts +82 -0
- package/dist/cache/mikro-orm.cache.d.ts.map +1 -0
- package/dist/cache/mikro-orm.cache.js +209 -0
- package/dist/cache/mikro-orm.cache.js.map +1 -0
- package/dist/concurrency/assert-autocommit.d.ts +21 -0
- package/dist/concurrency/assert-autocommit.d.ts.map +1 -0
- package/dist/concurrency/assert-autocommit.js +27 -0
- package/dist/concurrency/assert-autocommit.js.map +1 -0
- package/dist/concurrency/conditioned-write.d.ts +23 -0
- package/dist/concurrency/conditioned-write.d.ts.map +1 -0
- package/dist/concurrency/conditioned-write.js +26 -0
- package/dist/concurrency/conditioned-write.js.map +1 -0
- package/dist/concurrency/optimistic-delete.d.ts +52 -0
- package/dist/concurrency/optimistic-delete.d.ts.map +1 -0
- package/dist/concurrency/optimistic-delete.js +67 -0
- package/dist/concurrency/optimistic-delete.js.map +1 -0
- package/dist/concurrency/optimistic-update.d.ts +51 -0
- package/dist/concurrency/optimistic-update.d.ts.map +1 -0
- package/dist/concurrency/optimistic-update.js +60 -0
- package/dist/concurrency/optimistic-update.js.map +1 -0
- package/dist/errors/mikro-orm.dialect.d.ts +32 -0
- package/dist/errors/mikro-orm.dialect.d.ts.map +1 -0
- package/dist/errors/mikro-orm.dialect.js +96 -0
- package/dist/errors/mikro-orm.dialect.js.map +1 -0
- package/dist/errors/transient-error.d.ts +51 -0
- package/dist/errors/transient-error.d.ts.map +1 -0
- package/dist/errors/transient-error.js +97 -0
- package/dist/errors/transient-error.js.map +1 -0
- package/dist/helpers/sql-identifier.d.ts +19 -0
- package/dist/helpers/sql-identifier.d.ts.map +1 -0
- package/dist/helpers/sql-identifier.js +23 -0
- package/dist/helpers/sql-identifier.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/entity-manager-source.d.ts +19 -0
- package/dist/interfaces/entity-manager-source.d.ts.map +1 -0
- package/dist/interfaces/entity-manager-source.js +4 -0
- package/dist/interfaces/entity-manager-source.js.map +1 -0
- package/dist/mapping/root-entity.properties.d.ts +63 -0
- package/dist/mapping/root-entity.properties.d.ts.map +1 -0
- package/dist/mapping/root-entity.properties.js +63 -0
- package/dist/mapping/root-entity.properties.js.map +1 -0
- package/dist/mapping/unix-timestamp.type.d.ts +17 -0
- package/dist/mapping/unix-timestamp.type.d.ts.map +1 -0
- package/dist/mapping/unix-timestamp.type.js +55 -0
- package/dist/mapping/unix-timestamp.type.js.map +1 -0
- package/dist/repository/aggregate.repository.d.ts +68 -0
- package/dist/repository/aggregate.repository.d.ts.map +1 -0
- package/dist/repository/aggregate.repository.js +103 -0
- package/dist/repository/aggregate.repository.js.map +1 -0
- package/dist/tenancy/tenant-store.d.ts +63 -0
- package/dist/tenancy/tenant-store.d.ts.map +1 -0
- package/dist/tenancy/tenant-store.js +91 -0
- package/dist/tenancy/tenant-store.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.MikroOrmCache = void 0;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const persistence_1 = require("@cqrs-ddd/core/persistence");
|
|
7
|
+
const cache_entry_1 = require("./cache-entry");
|
|
8
|
+
/**
|
|
9
|
+
* Reads the committed row inside a CAS transaction, bypassing the identity map
|
|
10
|
+
* so a concurrent writer's revision is always observed.
|
|
11
|
+
*/
|
|
12
|
+
function readCommittedEntry(em, key) {
|
|
13
|
+
return em.findOne(cache_entry_1.CacheEntry, { key }, { refresh: true, disableIdentityMap: true });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Upper bound on compare-and-set re-reads. Each iteration only repeats after a
|
|
17
|
+
* competing writer won the row, so the bound exists to guarantee termination if
|
|
18
|
+
* the underlying store never reflects a write, not to limit real contention.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_CAS_ATTEMPTS = 16;
|
|
21
|
+
/** Raised when a compare-and-set loop cannot converge. */
|
|
22
|
+
class CacheCasExhaustedError extends Error {
|
|
23
|
+
constructor(operation, key) {
|
|
24
|
+
super(`MikroOrmCache.${operation} could not settle key "${key}" after ${MAX_CAS_ATTEMPTS} attempts.`);
|
|
25
|
+
this.name = 'CacheCasExhaustedError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* MikroORM-backed {@link IVersionedCache}: one row per key in the table mapped by
|
|
30
|
+
* {@link CacheEntrySchema} (or {@link createCacheEntrySchema}).
|
|
31
|
+
*
|
|
32
|
+
* Implements {@link IVersionedCache} with revision-fenced atomic coordination,
|
|
33
|
+
* bypassing the identity map across all read and mutation operations. Reads use
|
|
34
|
+
* `store.em`; every write runs in `store.transactional(...)`.
|
|
35
|
+
*
|
|
36
|
+
* A plain class with no framework dependency. In a NestJS application, register
|
|
37
|
+
* it with a factory:
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* {
|
|
42
|
+
* provide: CACHE_TOKEN,
|
|
43
|
+
* useFactory: (store: AppStore) =>
|
|
44
|
+
* new MikroOrmCache(store, { logger: new Logger('MikroOrmCache') }),
|
|
45
|
+
* inject: [STORE],
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
class MikroOrmCache {
|
|
50
|
+
store;
|
|
51
|
+
isVersioned = true;
|
|
52
|
+
logger;
|
|
53
|
+
defaultTtlMs;
|
|
54
|
+
constructor(store, options) {
|
|
55
|
+
this.store = store;
|
|
56
|
+
this.defaultTtlMs = options?.defaultTtlMs ?? 60_000;
|
|
57
|
+
this.logger = options?.logger ?? (0, persistence_1.consoleCacheLogger)('MikroOrmCache');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Observes the current key state and its revision token.
|
|
61
|
+
* Reads bypass the identity map to guarantee authoritative persistence visibility.
|
|
62
|
+
*/
|
|
63
|
+
async readState(key) {
|
|
64
|
+
const entry = await this.store.em.findOne(cache_entry_1.CacheEntry, { key }, { disableIdentityMap: true });
|
|
65
|
+
if (!entry) {
|
|
66
|
+
return { status: 'miss', revision: '0' };
|
|
67
|
+
}
|
|
68
|
+
const revision = (entry.revision ?? '0').toString();
|
|
69
|
+
if (!entry.value) {
|
|
70
|
+
return { status: 'miss', revision };
|
|
71
|
+
}
|
|
72
|
+
if (entry.expiresAt !== null && entry.expiresAt < Date.now()) {
|
|
73
|
+
return { status: 'expired', revision };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
status: 'hit',
|
|
77
|
+
value: JSON.parse(entry.value),
|
|
78
|
+
revision,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Atomically advances the revision for `key` and evicts the payload value.
|
|
83
|
+
* Retains the entry to prevent ABA races during concurrent fills.
|
|
84
|
+
*/
|
|
85
|
+
async invalidate(key) {
|
|
86
|
+
return this.store.transactional(async (em) => {
|
|
87
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
88
|
+
const existing = await readCommittedEntry(em, key);
|
|
89
|
+
if (!existing) {
|
|
90
|
+
// `onConflictAction: 'ignore'` keeps a lost insert race from aborting
|
|
91
|
+
// the surrounding transaction, so the loop can re-read and retry.
|
|
92
|
+
await em.upsert(cache_entry_1.CacheEntry, { key, value: '', expiresAt: null, revision: '1' }, { onConflictAction: 'ignore' });
|
|
93
|
+
const inserted = await readCommittedEntry(em, key);
|
|
94
|
+
if (inserted && (inserted.revision ?? '0').toString() === '1') {
|
|
95
|
+
return '1';
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const currentRev = (existing.revision ?? '0').toString();
|
|
100
|
+
const nextRevision = (BigInt(currentRev) + 1n).toString();
|
|
101
|
+
const affected = await em.nativeUpdate(cache_entry_1.CacheEntry, { key, revision: existing.revision }, {
|
|
102
|
+
value: '',
|
|
103
|
+
expiresAt: null,
|
|
104
|
+
revision: nextRevision,
|
|
105
|
+
});
|
|
106
|
+
if (affected > 0) {
|
|
107
|
+
return nextRevision;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new CacheCasExhaustedError('invalidate', key);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Atomically checks that current revision matches `observedRevision`, writes
|
|
115
|
+
* value, and advances revision on match.
|
|
116
|
+
*/
|
|
117
|
+
async tryFill(key, observedRevision, value, options) {
|
|
118
|
+
const ttl = options?.ttl ?? this.defaultTtlMs;
|
|
119
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : null;
|
|
120
|
+
const serialized = JSON.stringify(value);
|
|
121
|
+
return this.store.transactional(async (em) => {
|
|
122
|
+
const nextRevision = (BigInt(observedRevision) + 1n).toString();
|
|
123
|
+
if (observedRevision === '0') {
|
|
124
|
+
const existing = await readCommittedEntry(em, key);
|
|
125
|
+
if (!existing) {
|
|
126
|
+
await em.upsert(cache_entry_1.CacheEntry, { key, value: serialized, expiresAt, revision: '1' }, { onConflictAction: 'ignore' });
|
|
127
|
+
const inserted = await readCommittedEntry(em, key);
|
|
128
|
+
// A competing writer won the insert; this fill observed revision 0
|
|
129
|
+
// and must not overwrite it.
|
|
130
|
+
return inserted !== null && inserted.value === serialized;
|
|
131
|
+
}
|
|
132
|
+
if ((existing.revision ?? '0').toString() === '0') {
|
|
133
|
+
const affected = await em.nativeUpdate(cache_entry_1.CacheEntry, { key, revision: existing.revision }, {
|
|
134
|
+
value: serialized,
|
|
135
|
+
expiresAt,
|
|
136
|
+
revision: '1',
|
|
137
|
+
});
|
|
138
|
+
return affected > 0;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const affected = await em.nativeUpdate(cache_entry_1.CacheEntry, { key, revision: observedRevision }, {
|
|
143
|
+
value: serialized,
|
|
144
|
+
expiresAt,
|
|
145
|
+
revision: nextRevision,
|
|
146
|
+
});
|
|
147
|
+
return affected > 0;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get a value from the cache by key. Handles TTL expiry and returns detached clone.
|
|
152
|
+
*/
|
|
153
|
+
async get(key) {
|
|
154
|
+
const state = await this.readState(key);
|
|
155
|
+
return state.status === 'hit' ? state.value : undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Stores a value in the relational cache table with transactional CAS concurrency safety.
|
|
159
|
+
*/
|
|
160
|
+
async set(key, value, options) {
|
|
161
|
+
const ttl = options?.ttl ?? this.defaultTtlMs;
|
|
162
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : null;
|
|
163
|
+
const isNewer = options?.isNewer;
|
|
164
|
+
const serialized = JSON.stringify(value);
|
|
165
|
+
await this.store.transactional(async (em) => {
|
|
166
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
167
|
+
const existing = await readCommittedEntry(em, key);
|
|
168
|
+
if (!existing) {
|
|
169
|
+
await em.upsert(cache_entry_1.CacheEntry, { key, value: serialized, expiresAt, revision: '1' }, { onConflictAction: 'ignore' });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (isNewer && existing.value) {
|
|
173
|
+
if (existing.expiresAt === null || existing.expiresAt >= Date.now()) {
|
|
174
|
+
let parsed;
|
|
175
|
+
try {
|
|
176
|
+
parsed = JSON.parse(existing.value);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// An unparsable entry is treated as absent so the CAS update can heal corrupted data.
|
|
180
|
+
(0, persistence_1.safeWarn)(this.logger, `Failed to parse cached value (key digest: ${(0, node_crypto_1.createHash)('sha256').update(key).digest('hex')}); treating as absent.`);
|
|
181
|
+
}
|
|
182
|
+
if (parsed !== undefined && isNewer(parsed, value)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const currentRev = (existing.revision ?? '0').toString();
|
|
188
|
+
const nextRevision = (BigInt(currentRev) + 1n).toString();
|
|
189
|
+
const affected = await em.nativeUpdate(cache_entry_1.CacheEntry, { key, revision: existing.revision }, {
|
|
190
|
+
value: serialized,
|
|
191
|
+
expiresAt,
|
|
192
|
+
revision: nextRevision,
|
|
193
|
+
});
|
|
194
|
+
if (affected > 0)
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
throw new CacheCasExhaustedError('set', key);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Removes the value and advances the key's revision, as invalidate() does; the
|
|
202
|
+
* row is kept.
|
|
203
|
+
*/
|
|
204
|
+
async delete(key) {
|
|
205
|
+
await this.invalidate(key);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
exports.MikroOrmCache = MikroOrmCache;
|
|
209
|
+
//# sourceMappingURL=mikro-orm.cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mikro-orm.cache.js","sourceRoot":"","sources":["../../src/cache/mikro-orm.cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core';
|
|
2
|
+
/**
|
|
3
|
+
* Rejects an externally active transaction before a write that reports durable
|
|
4
|
+
* success.
|
|
5
|
+
*
|
|
6
|
+
* The lifecycle decorators treat a resolved `save()` as durable: `@AcknowledgePersisted`
|
|
7
|
+
* advances the aggregate's persisted version baseline and `@Cache` synchronises or
|
|
8
|
+
* invalidates cache entries. Inside a caller's transaction neither is true yet — the
|
|
9
|
+
* statement is buffered until a commit this code does not observe, so a later rollback
|
|
10
|
+
* would leave an acknowledged aggregate and a cache reflecting a write that never
|
|
11
|
+
* landed. Participating in an outer transaction needs commit callbacks or unit-of-work
|
|
12
|
+
* semantics; it must not be emulated by acknowledging early.
|
|
13
|
+
*
|
|
14
|
+
* Call it **before** issuing the statement, so a rejected operation mutates nothing.
|
|
15
|
+
*
|
|
16
|
+
* @param em The entity manager the operation will execute on.
|
|
17
|
+
* @param operation Name used in the error message, e.g. `'optimisticDelete'`.
|
|
18
|
+
* @throws {Error} When `em` is inside an active transaction.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertAutocommit(em: EntityManager, operation: string): void;
|
|
21
|
+
//# sourceMappingURL=assert-autocommit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assert-autocommit.d.ts","sourceRoot":"","sources":["../../src/concurrency/assert-autocommit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAM3E"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertAutocommit = assertAutocommit;
|
|
4
|
+
/**
|
|
5
|
+
* Rejects an externally active transaction before a write that reports durable
|
|
6
|
+
* success.
|
|
7
|
+
*
|
|
8
|
+
* The lifecycle decorators treat a resolved `save()` as durable: `@AcknowledgePersisted`
|
|
9
|
+
* advances the aggregate's persisted version baseline and `@Cache` synchronises or
|
|
10
|
+
* invalidates cache entries. Inside a caller's transaction neither is true yet — the
|
|
11
|
+
* statement is buffered until a commit this code does not observe, so a later rollback
|
|
12
|
+
* would leave an acknowledged aggregate and a cache reflecting a write that never
|
|
13
|
+
* landed. Participating in an outer transaction needs commit callbacks or unit-of-work
|
|
14
|
+
* semantics; it must not be emulated by acknowledging early.
|
|
15
|
+
*
|
|
16
|
+
* Call it **before** issuing the statement, so a rejected operation mutates nothing.
|
|
17
|
+
*
|
|
18
|
+
* @param em The entity manager the operation will execute on.
|
|
19
|
+
* @param operation Name used in the error message, e.g. `'optimisticDelete'`.
|
|
20
|
+
* @throws {Error} When `em` is inside an active transaction.
|
|
21
|
+
*/
|
|
22
|
+
function assertAutocommit(em, operation) {
|
|
23
|
+
if (em.isInTransaction()) {
|
|
24
|
+
throw new Error(`${operation} requires autocommit; external transactions need commit hooks.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=assert-autocommit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assert-autocommit.js","sourceRoot":"","sources":["../../src/concurrency/assert-autocommit.ts"],"names":[],"mappings":";;AAqBA,4CAMC;AAxBD;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,gBAAgB,CAAC,EAAiB,EAAE,SAAiB;IACnE,IAAI,EAAE,CAAC,eAAe,EAAE,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,GAAG,SAAS,gEAAgE,CAC7E,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EntityManager, EntityName } from '@mikro-orm/core';
|
|
2
|
+
/** An aggregate written with version checks, such as a `RootEntity`. */
|
|
3
|
+
export interface VersionedAggregate {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly version: number;
|
|
6
|
+
getExpectedVersion(): number;
|
|
7
|
+
}
|
|
8
|
+
/** The row a version-conditioned write targets, captured before it runs. */
|
|
9
|
+
export interface ConditionedRow {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly expectedVersion: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Checks the row count of a write filtered on `{ id, version }`: one row is
|
|
15
|
+
* success; none is diagnosed with one refreshed read on the same manager as
|
|
16
|
+
* a missing row or a version conflict; more breaks the primary key invariant.
|
|
17
|
+
*
|
|
18
|
+
* @throws {EntityNotFoundException} No row matched and the row is gone.
|
|
19
|
+
* @throws {ConcurrencyConflictError} No row matched because the version diverged.
|
|
20
|
+
* @throws {Error} More than one row was affected.
|
|
21
|
+
*/
|
|
22
|
+
export declare function expectOneRow<TEntity extends VersionedAggregate>(affected: number, em: EntityManager, entityType: EntityName<TEntity>, { id, expectedVersion }: ConditionedRow, entityName: string, verb: 'updated' | 'deleted'): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=conditioned-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditioned-write.d.ts","sourceRoot":"","sources":["../../src/concurrency/conditioned-write.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAe,MAAM,iBAAiB,CAAC;AAE9E,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kBAAkB,IAAI,MAAM,CAAC;CAC9B;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,SAAS,kBAAkB,EACnE,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,aAAa,EACjB,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,EAC/B,EAAE,EAAE,EAAE,eAAe,EAAE,EAAE,cAAc,EACvC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,SAAS,GAAG,SAAS,GAC1B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.expectOneRow = expectOneRow;
|
|
5
|
+
const domain_1 = require("@cqrs-ddd/core/domain");
|
|
6
|
+
/**
|
|
7
|
+
* Checks the row count of a write filtered on `{ id, version }`: one row is
|
|
8
|
+
* success; none is diagnosed with one refreshed read on the same manager as
|
|
9
|
+
* a missing row or a version conflict; more breaks the primary key invariant.
|
|
10
|
+
*
|
|
11
|
+
* @throws {EntityNotFoundException} No row matched and the row is gone.
|
|
12
|
+
* @throws {ConcurrencyConflictError} No row matched because the version diverged.
|
|
13
|
+
* @throws {Error} More than one row was affected.
|
|
14
|
+
*/
|
|
15
|
+
async function expectOneRow(affected, em, entityType, { id, expectedVersion }, entityName, verb) {
|
|
16
|
+
if (affected === 1)
|
|
17
|
+
return;
|
|
18
|
+
if (affected === 0) {
|
|
19
|
+
const existing = await em.findOne(entityType, { id }, { refresh: true });
|
|
20
|
+
if (!existing)
|
|
21
|
+
throw new domain_1.EntityNotFoundException(entityName, id);
|
|
22
|
+
throw new domain_1.ConcurrencyConflictError(entityName, id, expectedVersion, existing.version);
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Expected one ${verb} ${entityName}, received ${affected}.`);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=conditioned-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditioned-write.js","sourceRoot":"","sources":["../../src/concurrency/conditioned-write.ts"],"names":[],"mappings":";AAAA,sEAAsE;;AA8BtE,oCAwBC;AApDD,kDAG+B;AAgB/B;;;;;;;;GAQG;AACI,KAAK,UAAU,YAAY,CAChC,QAAgB,EAChB,EAAiB,EACjB,UAA+B,EAC/B,EAAE,EAAE,EAAE,eAAe,EAAkB,EACvC,UAAkB,EAClB,IAA2B;IAE3B,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO;IAC3B,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAC/B,UAAU,EACV,EAAE,EAAE,EAA0B,EAC9B,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAC;QACF,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,gCAAuB,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACjE,MAAM,IAAI,iCAAwB,CAChC,UAAU,EACV,EAAE,EACF,eAAe,EACf,QAAQ,CAAC,OAAO,CACjB,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,IAAI,UAAU,cAAc,QAAQ,GAAG,CAAC,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EntityManager, EntityName } from '@mikro-orm/core';
|
|
2
|
+
import { type VersionedAggregate } from './conditioned-write';
|
|
3
|
+
/**
|
|
4
|
+
* Executes a single version-conditioned, autocommitted `DELETE` on an aggregate.
|
|
5
|
+
*
|
|
6
|
+
* The delete counterpart of `optimisticUpdate`, with the same contract:
|
|
7
|
+
*
|
|
8
|
+
* - rejects an externally active transaction before touching the database
|
|
9
|
+
* ({@link assertAutocommit});
|
|
10
|
+
* - deletes on `{ id, version: aggregate.getExpectedVersion() }`, so a row
|
|
11
|
+
* modified since the aggregate was loaded is not removed;
|
|
12
|
+
* - one affected row is success;
|
|
13
|
+
* - zero rows runs one refreshed diagnostic read: an absent row is
|
|
14
|
+
* {@link EntityNotFoundException}, a present row is
|
|
15
|
+
* {@link ConcurrencyConflictError};
|
|
16
|
+
* - any other count is an invariant violation, since the filter includes a
|
|
17
|
+
* primary key.
|
|
18
|
+
*
|
|
19
|
+
* The caller passes `em` once and this helper uses exactly that instance for both
|
|
20
|
+
* the delete and the diagnostic read, so the two cannot observe different
|
|
21
|
+
* transactional state.
|
|
22
|
+
*
|
|
23
|
+
* Caching, aggregate acknowledgment and event publication stay with `@Cache`,
|
|
24
|
+
* `@AcknowledgePersisted` and `CommandBaseHandler`. A delete repository resolves
|
|
25
|
+
* to `null`: there is no new persisted snapshot to acknowledge.
|
|
26
|
+
*
|
|
27
|
+
* Rows without a version column are out of scope; delete them by primary key.
|
|
28
|
+
*
|
|
29
|
+
* @param em Entity manager in autocommit mode.
|
|
30
|
+
* @param entityType Entity class or registered name.
|
|
31
|
+
* @param aggregate Aggregate providing `id` and `getExpectedVersion()`.
|
|
32
|
+
* @param entityName Friendly name for diagnostics, e.g. `'User'`.
|
|
33
|
+
*
|
|
34
|
+
* @throws {Error} Inside an active transaction, or on an unexpected row count.
|
|
35
|
+
* @throws {EntityNotFoundException} No row matched and the entity is gone.
|
|
36
|
+
* @throws {ConcurrencyConflictError} No row matched because the version diverged.
|
|
37
|
+
*
|
|
38
|
+
* @example Delete command repository
|
|
39
|
+
* ```typescript
|
|
40
|
+
* @Cache<User, null>({ deleteKeys: (user) => [cacheKey(User.aggregateName, { id: user.id })] })
|
|
41
|
+
* @MapPersistenceErrors<[User], User>({
|
|
42
|
+
* entity: ([user]) => user,
|
|
43
|
+
* otherwise: (error, user) => mapPersistenceError(error, `deleting User ${user.id}`),
|
|
44
|
+
* })
|
|
45
|
+
* async save(user: User): Promise<null> {
|
|
46
|
+
* await optimisticDelete(this.store.em, User, user, 'User');
|
|
47
|
+
* return null;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function optimisticDelete<TEntity extends VersionedAggregate>(em: EntityManager, entityType: EntityName<TEntity>, aggregate: TEntity, entityName: string): Promise<void>;
|
|
52
|
+
//# sourceMappingURL=optimistic-delete.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimistic-delete.d.ts","sourceRoot":"","sources":["../../src/concurrency/optimistic-delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAe,MAAM,iBAAiB,CAAC;AAE9E,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,SAAS,kBAAkB,EACvE,EAAE,EAAE,aAAa,EACjB,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,EAC/B,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAWf"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.optimisticDelete = optimisticDelete;
|
|
5
|
+
const assert_autocommit_1 = require("./assert-autocommit");
|
|
6
|
+
const conditioned_write_1 = require("./conditioned-write");
|
|
7
|
+
/**
|
|
8
|
+
* Executes a single version-conditioned, autocommitted `DELETE` on an aggregate.
|
|
9
|
+
*
|
|
10
|
+
* The delete counterpart of `optimisticUpdate`, with the same contract:
|
|
11
|
+
*
|
|
12
|
+
* - rejects an externally active transaction before touching the database
|
|
13
|
+
* ({@link assertAutocommit});
|
|
14
|
+
* - deletes on `{ id, version: aggregate.getExpectedVersion() }`, so a row
|
|
15
|
+
* modified since the aggregate was loaded is not removed;
|
|
16
|
+
* - one affected row is success;
|
|
17
|
+
* - zero rows runs one refreshed diagnostic read: an absent row is
|
|
18
|
+
* {@link EntityNotFoundException}, a present row is
|
|
19
|
+
* {@link ConcurrencyConflictError};
|
|
20
|
+
* - any other count is an invariant violation, since the filter includes a
|
|
21
|
+
* primary key.
|
|
22
|
+
*
|
|
23
|
+
* The caller passes `em` once and this helper uses exactly that instance for both
|
|
24
|
+
* the delete and the diagnostic read, so the two cannot observe different
|
|
25
|
+
* transactional state.
|
|
26
|
+
*
|
|
27
|
+
* Caching, aggregate acknowledgment and event publication stay with `@Cache`,
|
|
28
|
+
* `@AcknowledgePersisted` and `CommandBaseHandler`. A delete repository resolves
|
|
29
|
+
* to `null`: there is no new persisted snapshot to acknowledge.
|
|
30
|
+
*
|
|
31
|
+
* Rows without a version column are out of scope; delete them by primary key.
|
|
32
|
+
*
|
|
33
|
+
* @param em Entity manager in autocommit mode.
|
|
34
|
+
* @param entityType Entity class or registered name.
|
|
35
|
+
* @param aggregate Aggregate providing `id` and `getExpectedVersion()`.
|
|
36
|
+
* @param entityName Friendly name for diagnostics, e.g. `'User'`.
|
|
37
|
+
*
|
|
38
|
+
* @throws {Error} Inside an active transaction, or on an unexpected row count.
|
|
39
|
+
* @throws {EntityNotFoundException} No row matched and the entity is gone.
|
|
40
|
+
* @throws {ConcurrencyConflictError} No row matched because the version diverged.
|
|
41
|
+
*
|
|
42
|
+
* @example Delete command repository
|
|
43
|
+
* ```typescript
|
|
44
|
+
* @Cache<User, null>({ deleteKeys: (user) => [cacheKey(User.aggregateName, { id: user.id })] })
|
|
45
|
+
* @MapPersistenceErrors<[User], User>({
|
|
46
|
+
* entity: ([user]) => user,
|
|
47
|
+
* otherwise: (error, user) => mapPersistenceError(error, `deleting User ${user.id}`),
|
|
48
|
+
* })
|
|
49
|
+
* async save(user: User): Promise<null> {
|
|
50
|
+
* await optimisticDelete(this.store.em, User, user, 'User');
|
|
51
|
+
* return null;
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
async function optimisticDelete(em, entityType, aggregate, entityName) {
|
|
56
|
+
(0, assert_autocommit_1.assertAutocommit)(em, 'optimisticDelete');
|
|
57
|
+
const row = {
|
|
58
|
+
id: aggregate.id,
|
|
59
|
+
expectedVersion: aggregate.getExpectedVersion(),
|
|
60
|
+
};
|
|
61
|
+
const affected = await em.nativeDelete(entityType, {
|
|
62
|
+
id: row.id,
|
|
63
|
+
version: row.expectedVersion,
|
|
64
|
+
});
|
|
65
|
+
await (0, conditioned_write_1.expectOneRow)(affected, em, entityType, row, entityName, 'deleted');
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=optimistic-delete.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimistic-delete.js","sourceRoot":"","sources":["../../src/concurrency/optimistic-delete.ts"],"names":[],"mappings":";AAAA,sEAAsE;;AAsDtE,4CAgBC;AAnED,2DAAuD;AACvD,2DAA4E;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACI,KAAK,UAAU,gBAAgB,CACpC,EAAiB,EACjB,UAA+B,EAC/B,SAAkB,EAClB,UAAkB;IAElB,IAAA,oCAAgB,EAAC,EAAE,EAAE,kBAAkB,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG;QACV,EAAE,EAAE,SAAS,CAAC,EAAE;QAChB,eAAe,EAAE,SAAS,CAAC,kBAAkB,EAAE;KAChD,CAAC;IACF,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE;QACjD,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,OAAO,EAAE,GAAG,CAAC,eAAe;KACL,CAAC,CAAC;IAC3B,MAAM,IAAA,gCAAY,EAAC,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { EntityData, EntityManager, EntityName } from '@mikro-orm/core';
|
|
2
|
+
import { type VersionedAggregate } from './conditioned-write';
|
|
3
|
+
/**
|
|
4
|
+
* Executes a single, version-conditioned, autocommitted SQL `UPDATE` statement on an aggregate entity.
|
|
5
|
+
*
|
|
6
|
+
* This function handles low-level optimistic locking mechanics for update command repositories:
|
|
7
|
+
* - **Autocommit Enforcement**: Rejects an externally active transaction via
|
|
8
|
+
* {@link assertAutocommit}, because multi-statement transactions require commit hooks
|
|
9
|
+
* rather than standalone method decorators to guarantee atomic acknowledgment.
|
|
10
|
+
* - **Version-Conditioned Write**: Issues `em.nativeUpdate` with filter `{ id: entity.id, version: entity.getExpectedVersion() }`
|
|
11
|
+
* and updates fields including `{ version: entity.version }`.
|
|
12
|
+
* - **Affected Row Verification**:
|
|
13
|
+
* - Exactly `1` row affected: Write succeeded; completes cleanly.
|
|
14
|
+
* - `0` rows affected: Performs a refreshed diagnostic read (`findOne` with `refresh: true`):
|
|
15
|
+
* - If the entity is absent, throws {@link EntityNotFoundException}.
|
|
16
|
+
* - If the entity is present, throws {@link ConcurrencyConflictError}.
|
|
17
|
+
* - More than `1` row affected: Throws an `Error` indicating primary-key uniqueness invariant violation.
|
|
18
|
+
*
|
|
19
|
+
* > [!NOTE]
|
|
20
|
+
* > This helper does not perform caching, aggregate acknowledgment, or domain event publication.
|
|
21
|
+
* > Those concerns are owned by `@Cache`, `@AcknowledgePersisted`, and `CommandBaseHandler` respectively.
|
|
22
|
+
*
|
|
23
|
+
* @param em The MikroORM {@link EntityManager} instance (must be in autocommit mode).
|
|
24
|
+
* @param entityType The entity class or registered name (e.g. `User`, `Role`).
|
|
25
|
+
* @param entity The aggregate instance providing `id`, `version`, and `getExpectedVersion()`.
|
|
26
|
+
* @param data Field payload to update (the `version` field is automatically injected from `entity.version`).
|
|
27
|
+
* @param entityName Friendly name of the entity for error diagnostics (e.g. `'User'`).
|
|
28
|
+
*
|
|
29
|
+
* @throws {Error} If called within an active transaction (`em.isInTransaction() === true`).
|
|
30
|
+
* @throws {EntityNotFoundException} If 0 rows were updated and the entity cannot be found.
|
|
31
|
+
* @throws {ConcurrencyConflictError} If 0 rows were updated and the entity version has diverged.
|
|
32
|
+
* @throws {Error} If unexpected row count (> 1) was affected.
|
|
33
|
+
*
|
|
34
|
+
* @example Usage in an update command repository
|
|
35
|
+
* ```typescript
|
|
36
|
+
* @PersistedWrite<User>({ cache: { setKey: (user) => cacheKey(User.aggregateName, { id: user.id }) } })
|
|
37
|
+
* async save(user: User): Promise<UserSnapshot> {
|
|
38
|
+
* const snapshot = user.toJSON();
|
|
39
|
+
* await optimisticUpdate(
|
|
40
|
+
* this.store.em,
|
|
41
|
+
* User,
|
|
42
|
+
* user,
|
|
43
|
+
* { username: snapshot.username, updatedAt: snapshot.updatedAt },
|
|
44
|
+
* 'User',
|
|
45
|
+
* );
|
|
46
|
+
* return snapshot;
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function optimisticUpdate<TEntity extends VersionedAggregate>(em: EntityManager, entityType: EntityName<TEntity>, entity: TEntity, data: EntityData<TEntity>, entityName: string): Promise<void>;
|
|
51
|
+
//# sourceMappingURL=optimistic-update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimistic-update.d.ts","sourceRoot":"","sources":["../../src/concurrency/optimistic-update.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,UAAU,EAEX,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,SAAS,kBAAkB,EACvE,EAAE,EAAE,aAAa,EACjB,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,EAC/B,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,EACzB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.optimisticUpdate = optimisticUpdate;
|
|
5
|
+
const assert_autocommit_1 = require("./assert-autocommit");
|
|
6
|
+
const conditioned_write_1 = require("./conditioned-write");
|
|
7
|
+
/**
|
|
8
|
+
* Executes a single, version-conditioned, autocommitted SQL `UPDATE` statement on an aggregate entity.
|
|
9
|
+
*
|
|
10
|
+
* This function handles low-level optimistic locking mechanics for update command repositories:
|
|
11
|
+
* - **Autocommit Enforcement**: Rejects an externally active transaction via
|
|
12
|
+
* {@link assertAutocommit}, because multi-statement transactions require commit hooks
|
|
13
|
+
* rather than standalone method decorators to guarantee atomic acknowledgment.
|
|
14
|
+
* - **Version-Conditioned Write**: Issues `em.nativeUpdate` with filter `{ id: entity.id, version: entity.getExpectedVersion() }`
|
|
15
|
+
* and updates fields including `{ version: entity.version }`.
|
|
16
|
+
* - **Affected Row Verification**:
|
|
17
|
+
* - Exactly `1` row affected: Write succeeded; completes cleanly.
|
|
18
|
+
* - `0` rows affected: Performs a refreshed diagnostic read (`findOne` with `refresh: true`):
|
|
19
|
+
* - If the entity is absent, throws {@link EntityNotFoundException}.
|
|
20
|
+
* - If the entity is present, throws {@link ConcurrencyConflictError}.
|
|
21
|
+
* - More than `1` row affected: Throws an `Error` indicating primary-key uniqueness invariant violation.
|
|
22
|
+
*
|
|
23
|
+
* > [!NOTE]
|
|
24
|
+
* > This helper does not perform caching, aggregate acknowledgment, or domain event publication.
|
|
25
|
+
* > Those concerns are owned by `@Cache`, `@AcknowledgePersisted`, and `CommandBaseHandler` respectively.
|
|
26
|
+
*
|
|
27
|
+
* @param em The MikroORM {@link EntityManager} instance (must be in autocommit mode).
|
|
28
|
+
* @param entityType The entity class or registered name (e.g. `User`, `Role`).
|
|
29
|
+
* @param entity The aggregate instance providing `id`, `version`, and `getExpectedVersion()`.
|
|
30
|
+
* @param data Field payload to update (the `version` field is automatically injected from `entity.version`).
|
|
31
|
+
* @param entityName Friendly name of the entity for error diagnostics (e.g. `'User'`).
|
|
32
|
+
*
|
|
33
|
+
* @throws {Error} If called within an active transaction (`em.isInTransaction() === true`).
|
|
34
|
+
* @throws {EntityNotFoundException} If 0 rows were updated and the entity cannot be found.
|
|
35
|
+
* @throws {ConcurrencyConflictError} If 0 rows were updated and the entity version has diverged.
|
|
36
|
+
* @throws {Error} If unexpected row count (> 1) was affected.
|
|
37
|
+
*
|
|
38
|
+
* @example Usage in an update command repository
|
|
39
|
+
* ```typescript
|
|
40
|
+
* @PersistedWrite<User>({ cache: { setKey: (user) => cacheKey(User.aggregateName, { id: user.id }) } })
|
|
41
|
+
* async save(user: User): Promise<UserSnapshot> {
|
|
42
|
+
* const snapshot = user.toJSON();
|
|
43
|
+
* await optimisticUpdate(
|
|
44
|
+
* this.store.em,
|
|
45
|
+
* User,
|
|
46
|
+
* user,
|
|
47
|
+
* { username: snapshot.username, updatedAt: snapshot.updatedAt },
|
|
48
|
+
* 'User',
|
|
49
|
+
* );
|
|
50
|
+
* return snapshot;
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
async function optimisticUpdate(em, entityType, entity, data, entityName) {
|
|
55
|
+
(0, assert_autocommit_1.assertAutocommit)(em, 'optimisticUpdate');
|
|
56
|
+
const row = { id: entity.id, expectedVersion: entity.getExpectedVersion() };
|
|
57
|
+
const affected = await em.nativeUpdate(entityType, { id: row.id, version: row.expectedVersion }, { ...data, version: entity.version });
|
|
58
|
+
await (0, conditioned_write_1.expectOneRow)(affected, em, entityType, row, entityName, 'updated');
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=optimistic-update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimistic-update.js","sourceRoot":"","sources":["../../src/concurrency/optimistic-update.ts"],"names":[],"mappings":";AAAA,sEAAsE;;AA0DtE,4CAeC;AAjED,2DAAuD;AACvD,2DAA4E;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACI,KAAK,UAAU,gBAAgB,CACpC,EAAiB,EACjB,UAA+B,EAC/B,MAAe,EACf,IAAyB,EACzB,UAAkB;IAElB,IAAA,oCAAgB,EAAC,EAAE,EAAE,kBAAkB,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,eAAe,EAAE,MAAM,CAAC,kBAAkB,EAAE,EAAE,CAAC;IAC5E,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,YAAY,CACpC,UAAU,EACV,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,eAAe,EAA0B,EACpE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAyB,CAC5D,CAAC;IACF,MAAM,IAAA,gCAAY,EAAC,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IPersistenceDialect } from '@cqrs-ddd/core/persistence';
|
|
2
|
+
import { type MikroORM } from '@mikro-orm/core';
|
|
3
|
+
/**
|
|
4
|
+
* The MikroORM persistence dialect: it reads the violated unique constraint of a
|
|
5
|
+
* `UniqueConstraintViolationException` through the ORM's entity metadata, so a
|
|
6
|
+
* repository maps it by entity property and never names a constraint or column.
|
|
7
|
+
*
|
|
8
|
+
* - PostgreSQL reports the constraint name; it is matched against the names in
|
|
9
|
+
* the mapping (`unique: 'name'` on a property, `uniques[].name`) or, when none
|
|
10
|
+
* is declared, the naming strategy's `indexName`.
|
|
11
|
+
* - SQLite and libSQL report the `table.column` list; it is matched against the
|
|
12
|
+
* columns of each constraint.
|
|
13
|
+
*
|
|
14
|
+
* Any other error, an entity without metadata, or an unknown constraint yields
|
|
15
|
+
* `undefined`, so the decorator rethrows the original error.
|
|
16
|
+
*
|
|
17
|
+
* @throws {TypeError} On construction, when a multi-property unique constraint
|
|
18
|
+
* has no name, or two constraints of one entity cover the same columns.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const orm = await MikroORM.init(options);
|
|
23
|
+
* setPersistenceDialect(new MikroOrmDialect(orm));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class MikroOrmDialect implements IPersistenceDialect {
|
|
27
|
+
private readonly constraints;
|
|
28
|
+
private readonly metadata;
|
|
29
|
+
constructor(orm: Pick<MikroORM, 'getMetadata' | 'config'>);
|
|
30
|
+
uniqueViolation(error: unknown, entity: object): string | undefined;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=mikro-orm.dialect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mikro-orm.dialect.d.ts","sourceRoot":"","sources":["../../src/errors/mikro-orm.dialect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAGL,KAAK,QAAQ,EAGd,MAAM,iBAAiB,CAAC;AA0EzB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,eAAgB,YAAW,mBAAmB;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;IACJ,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;gBAE/B,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,QAAQ,CAAC;IAWzD,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAapE"}
|