node-cqrs 1.2.0-beta.0 → 1.3.0-alpha.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 (82) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +29 -9
  3. package/dist/cjs/postgresql/AbstractPostgresqlAccessor.js +83 -0
  4. package/dist/cjs/postgresql/AbstractPostgresqlAccessor.js.map +1 -0
  5. package/dist/cjs/postgresql/AbstractPostgresqlObjectProjection.js +36 -0
  6. package/dist/cjs/postgresql/AbstractPostgresqlObjectProjection.js.map +1 -0
  7. package/dist/cjs/postgresql/AbstractPostgresqlView.js +53 -0
  8. package/dist/cjs/postgresql/AbstractPostgresqlView.js.map +1 -0
  9. package/dist/cjs/postgresql/IContainer.js +3 -0
  10. package/dist/cjs/postgresql/IContainer.js.map +1 -0
  11. package/dist/cjs/postgresql/PostgresqlConnection.js +3 -0
  12. package/dist/cjs/postgresql/PostgresqlConnection.js.map +1 -0
  13. package/dist/cjs/postgresql/PostgresqlEventLocker.js +127 -0
  14. package/dist/cjs/postgresql/PostgresqlEventLocker.js.map +1 -0
  15. package/dist/cjs/postgresql/PostgresqlEventStorage.js +315 -0
  16. package/dist/cjs/postgresql/PostgresqlEventStorage.js.map +1 -0
  17. package/dist/cjs/postgresql/PostgresqlObjectStorage.js +137 -0
  18. package/dist/cjs/postgresql/PostgresqlObjectStorage.js.map +1 -0
  19. package/dist/cjs/postgresql/PostgresqlObjectView.js +46 -0
  20. package/dist/cjs/postgresql/PostgresqlObjectView.js.map +1 -0
  21. package/dist/cjs/postgresql/PostgresqlProjectionDataParams.js +3 -0
  22. package/dist/cjs/postgresql/PostgresqlProjectionDataParams.js.map +1 -0
  23. package/dist/cjs/postgresql/PostgresqlViewLocker.js +151 -0
  24. package/dist/cjs/postgresql/PostgresqlViewLocker.js.map +1 -0
  25. package/dist/cjs/postgresql/index.js +29 -0
  26. package/dist/cjs/postgresql/index.js.map +1 -0
  27. package/dist/cjs/postgresql/utils/getEventId.js +14 -0
  28. package/dist/cjs/postgresql/utils/getEventId.js.map +1 -0
  29. package/dist/cjs/postgresql/utils/index.js +19 -0
  30. package/dist/cjs/postgresql/utils/index.js.map +1 -0
  31. package/dist/cjs/postgresql/utils/quoteIdentifier.js +14 -0
  32. package/dist/cjs/postgresql/utils/quoteIdentifier.js.map +1 -0
  33. package/dist/cjs/sqlite/SqliteEventStorage.js +66 -36
  34. package/dist/cjs/sqlite/SqliteEventStorage.js.map +1 -1
  35. package/dist/esm/postgresql/AbstractPostgresqlAccessor.js +79 -0
  36. package/dist/esm/postgresql/AbstractPostgresqlAccessor.js.map +1 -0
  37. package/dist/esm/postgresql/AbstractPostgresqlObjectProjection.js +32 -0
  38. package/dist/esm/postgresql/AbstractPostgresqlObjectProjection.js.map +1 -0
  39. package/dist/esm/postgresql/AbstractPostgresqlView.js +49 -0
  40. package/dist/esm/postgresql/AbstractPostgresqlView.js.map +1 -0
  41. package/dist/esm/postgresql/IContainer.js +2 -0
  42. package/dist/esm/postgresql/IContainer.js.map +1 -0
  43. package/dist/esm/postgresql/PostgresqlConnection.js +2 -0
  44. package/dist/esm/postgresql/PostgresqlConnection.js.map +1 -0
  45. package/dist/esm/postgresql/PostgresqlEventLocker.js +123 -0
  46. package/dist/esm/postgresql/PostgresqlEventLocker.js.map +1 -0
  47. package/dist/esm/postgresql/PostgresqlEventStorage.js +311 -0
  48. package/dist/esm/postgresql/PostgresqlEventStorage.js.map +1 -0
  49. package/dist/esm/postgresql/PostgresqlObjectStorage.js +133 -0
  50. package/dist/esm/postgresql/PostgresqlObjectStorage.js.map +1 -0
  51. package/dist/esm/postgresql/PostgresqlObjectView.js +42 -0
  52. package/dist/esm/postgresql/PostgresqlObjectView.js.map +1 -0
  53. package/dist/esm/postgresql/PostgresqlProjectionDataParams.js +2 -0
  54. package/dist/esm/postgresql/PostgresqlProjectionDataParams.js.map +1 -0
  55. package/dist/esm/postgresql/PostgresqlViewLocker.js +147 -0
  56. package/dist/esm/postgresql/PostgresqlViewLocker.js.map +1 -0
  57. package/dist/esm/postgresql/index.js +13 -0
  58. package/dist/esm/postgresql/index.js.map +1 -0
  59. package/dist/esm/postgresql/utils/getEventId.js +10 -0
  60. package/dist/esm/postgresql/utils/getEventId.js.map +1 -0
  61. package/dist/esm/postgresql/utils/index.js +3 -0
  62. package/dist/esm/postgresql/utils/index.js.map +1 -0
  63. package/dist/esm/postgresql/utils/quoteIdentifier.js +11 -0
  64. package/dist/esm/postgresql/utils/quoteIdentifier.js.map +1 -0
  65. package/dist/esm/sqlite/SqliteEventStorage.js +66 -36
  66. package/dist/esm/sqlite/SqliteEventStorage.js.map +1 -1
  67. package/dist/types/postgresql/AbstractPostgresqlAccessor.d.ts +29 -0
  68. package/dist/types/postgresql/AbstractPostgresqlObjectProjection.d.ts +12 -0
  69. package/dist/types/postgresql/AbstractPostgresqlView.d.ts +23 -0
  70. package/dist/types/postgresql/IContainer.d.ts +12 -0
  71. package/dist/types/postgresql/PostgresqlConnection.d.ts +7 -0
  72. package/dist/types/postgresql/PostgresqlEventLocker.d.ts +39 -0
  73. package/dist/types/postgresql/PostgresqlEventStorage.d.ts +23 -0
  74. package/dist/types/postgresql/PostgresqlObjectStorage.d.ts +29 -0
  75. package/dist/types/postgresql/PostgresqlObjectView.d.ts +19 -0
  76. package/dist/types/postgresql/PostgresqlProjectionDataParams.d.ts +14 -0
  77. package/dist/types/postgresql/PostgresqlViewLocker.d.ts +41 -0
  78. package/dist/types/postgresql/index.d.ts +12 -0
  79. package/dist/types/postgresql/utils/getEventId.d.ts +5 -0
  80. package/dist/types/postgresql/utils/index.d.ts +2 -0
  81. package/dist/types/postgresql/utils/quoteIdentifier.d.ts +1 -0
  82. package/package.json +20 -5
@@ -0,0 +1,123 @@
1
+ import { assertNonNegativeInteger, assertString } from "../utils/assert.js";
2
+ import { AbstractPostgresqlAccessor } from "./AbstractPostgresqlAccessor.js";
3
+ import { getEventId, quoteIdentifier } from "./utils/index.js";
4
+ /**
5
+ * PostgreSQL-backed implementation of IEventLocker.
6
+ *
7
+ * Uses one table for per-event processing locks and the view lock table for the
8
+ * last processed event checkpoint.
9
+ */
10
+ export class PostgresqlEventLocker extends AbstractPostgresqlAccessor {
11
+ static DEFAULT_EVENT_LOCK_TTL = 15_000;
12
+ static DEFAULT_EVENT_LOCK_TABLE = 'ncqrs_event_locks';
13
+ static DEFAULT_VIEW_LOCK_TABLE = 'ncqrs_view_locks';
14
+ #projectionName;
15
+ #schemaVersion;
16
+ #viewLockTableName;
17
+ #eventLockTableName;
18
+ #eventLockTtl;
19
+ constructor(o) {
20
+ super(o);
21
+ assertString(o.projectionName, 'o.projectionName');
22
+ assertString(o.schemaVersion, 'o.schemaVersion');
23
+ if (o.viewLockTableName !== undefined)
24
+ assertString(o.viewLockTableName, 'o.viewLockTableName');
25
+ if (o.eventLockTableName !== undefined)
26
+ assertString(o.eventLockTableName, 'o.eventLockTableName');
27
+ if (o.eventLockTtl !== undefined)
28
+ assertNonNegativeInteger(o.eventLockTtl, 'o.eventLockTtl');
29
+ this.#projectionName = o.projectionName;
30
+ this.#schemaVersion = o.schemaVersion;
31
+ this.#viewLockTableName = quoteIdentifier(o.viewLockTableName ?? PostgresqlEventLocker.DEFAULT_VIEW_LOCK_TABLE);
32
+ this.#eventLockTableName = quoteIdentifier(o.eventLockTableName ?? PostgresqlEventLocker.DEFAULT_EVENT_LOCK_TABLE);
33
+ this.#eventLockTtl = o.eventLockTtl ?? PostgresqlEventLocker.DEFAULT_EVENT_LOCK_TTL;
34
+ }
35
+ async initialize(db) {
36
+ await db.query(`
37
+ CREATE TABLE IF NOT EXISTS ${this.#viewLockTableName} (
38
+ projection_name text NOT NULL,
39
+ schema_version text NOT NULL,
40
+ locked_till timestamptz NULL,
41
+ lock_token text NULL,
42
+ last_event text NULL,
43
+ PRIMARY KEY (projection_name, schema_version)
44
+ )
45
+ `);
46
+ await db.query(`
47
+ CREATE TABLE IF NOT EXISTS ${this.#eventLockTableName} (
48
+ projection_name text NOT NULL,
49
+ schema_version text NOT NULL,
50
+ event_id text NOT NULL,
51
+ processing_at timestamptz NOT NULL DEFAULT NOW(),
52
+ processed_at timestamptz NULL,
53
+ PRIMARY KEY (projection_name, schema_version, event_id)
54
+ )
55
+ `);
56
+ await db.query(`
57
+ CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${this.#tableNameForIndex()}_processing_at_idx`)}
58
+ ON ${this.#eventLockTableName} (processing_at)
59
+ `);
60
+ }
61
+ async tryMarkAsProjecting(event) {
62
+ await this.assertConnection();
63
+ const eventId = getEventId(event);
64
+ const r = await this.connection.query(`
65
+ INSERT INTO ${this.#eventLockTableName} AS event_lock
66
+ (projection_name, schema_version, event_id, processing_at, processed_at)
67
+ VALUES ($1, $2, $3, NOW(), NULL)
68
+ ON CONFLICT (projection_name, schema_version, event_id)
69
+ DO UPDATE SET
70
+ processing_at = NOW(),
71
+ processed_at = NULL
72
+ WHERE
73
+ event_lock.processed_at IS NULL
74
+ AND event_lock.processing_at <= NOW() - ($4::double precision * INTERVAL '1 millisecond')
75
+ `, [this.#projectionName, this.#schemaVersion, eventId, this.#eventLockTtl]);
76
+ return r.rowCount !== 0;
77
+ }
78
+ async markAsProjected(event) {
79
+ await this.assertConnection();
80
+ const eventId = getEventId(event);
81
+ const result = await this.connection.query(`
82
+ UPDATE ${this.#eventLockTableName}
83
+ SET
84
+ processed_at = NOW()
85
+ WHERE
86
+ projection_name = $1
87
+ AND schema_version = $2
88
+ AND event_id = $3
89
+ AND processed_at IS NULL
90
+ `, [this.#projectionName, this.#schemaVersion, eventId]);
91
+ if (result.rowCount !== 1)
92
+ throw new Error(`Event ${event.id} could not be marked as processed`);
93
+ }
94
+ async markAsLastEvent(event) {
95
+ await this.assertConnection();
96
+ await this.connection.query(`
97
+ INSERT INTO ${this.#viewLockTableName} (projection_name, schema_version, last_event)
98
+ VALUES ($1, $2, $3)
99
+ ON CONFLICT (projection_name, schema_version)
100
+ DO UPDATE SET
101
+ last_event = excluded.last_event
102
+ `, [this.#projectionName, this.#schemaVersion, JSON.stringify(event)]);
103
+ }
104
+ async getLastEvent() {
105
+ await this.assertConnection();
106
+ const viewInfoRecord = await this.connection.query(`
107
+ SELECT
108
+ last_event
109
+ FROM ${this.#viewLockTableName}
110
+ WHERE
111
+ projection_name = $1
112
+ AND schema_version = $2
113
+ `, [this.#projectionName, this.#schemaVersion]);
114
+ const lastEvent = viewInfoRecord.rows[0]?.last_event;
115
+ if (!lastEvent)
116
+ return undefined;
117
+ return JSON.parse(lastEvent);
118
+ }
119
+ #tableNameForIndex() {
120
+ return this.#eventLockTableName.replaceAll('"', '').replaceAll('.', '_');
121
+ }
122
+ }
123
+ //# sourceMappingURL=PostgresqlEventLocker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostgresqlEventLocker.js","sourceRoot":"","sources":["../../../src/postgresql/PostgresqlEventLocker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,311 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ConcurrencyError } from "../errors/index.js";
3
+ import { assertString, parseSagaId } from "../utils/index.js";
4
+ import { AbstractPostgresqlAccessor } from "./AbstractPostgresqlAccessor.js";
5
+ import { quoteIdentifier } from "./utils/index.js";
6
+ function extractMeta(envelope) {
7
+ const { event: _event, ignoreConcurrencyError: _ignore, ...rest } = envelope;
8
+ if (Object.keys(rest).length === 0)
9
+ return null;
10
+ return rest;
11
+ }
12
+ function parseJson(value) {
13
+ if (typeof value !== 'string')
14
+ return value;
15
+ return JSON.parse(value);
16
+ }
17
+ function indexPrefix(tableName) {
18
+ return tableName.replaceAll('"', '').replaceAll('.', '_');
19
+ }
20
+ function reconstructEvent(row) {
21
+ const data = parseJson(row.data);
22
+ const event = {
23
+ id: row.id,
24
+ ...data
25
+ };
26
+ if (row.saga_origins)
27
+ event.sagaOrigins = parseJson(row.saga_origins);
28
+ return event;
29
+ }
30
+ export class PostgresqlEventStorage extends AbstractPostgresqlAccessor {
31
+ static EVENTS_TABLE = 'tbl_events';
32
+ static EVENT_SAGAS_TABLE = 'tbl_event_sagas';
33
+ #eventsTableName;
34
+ #eventSagasTableName;
35
+ #aggregateVersionIndexName;
36
+ constructor({ postgresqlEventStorageConfig, viewModelPostgresqlDb, viewModelPostgresqlDbFactory }) {
37
+ super({ viewModelPostgresqlDb, viewModelPostgresqlDbFactory });
38
+ const eventsTableName = postgresqlEventStorageConfig?.eventsTableName ?? PostgresqlEventStorage.EVENTS_TABLE;
39
+ const eventSagasTableName = postgresqlEventStorageConfig?.eventSagasTableName ??
40
+ PostgresqlEventStorage.EVENT_SAGAS_TABLE;
41
+ assertString(eventsTableName, 'postgresqlEventStorageConfig.eventsTableName');
42
+ assertString(eventSagasTableName, 'postgresqlEventStorageConfig.eventSagasTableName');
43
+ this.#eventsTableName = quoteIdentifier(eventsTableName);
44
+ this.#eventSagasTableName = quoteIdentifier(eventSagasTableName);
45
+ this.#aggregateVersionIndexName = `${indexPrefix(this.#eventsTableName)}_aggregate_version_unique_idx`;
46
+ }
47
+ async initialize(db) {
48
+ await db.query(`
49
+ CREATE TABLE IF NOT EXISTS ${this.#eventsTableName} (
50
+ position BIGSERIAL PRIMARY KEY,
51
+ id TEXT NOT NULL UNIQUE,
52
+ aggregate_id TEXT,
53
+ aggregate_version INTEGER,
54
+ type TEXT NOT NULL,
55
+ data JSONB NOT NULL,
56
+ meta JSONB,
57
+ check_concurrency BOOLEAN NOT NULL DEFAULT TRUE
58
+ )
59
+ `);
60
+ await db.query(`
61
+ CREATE TABLE IF NOT EXISTS ${this.#eventSagasTableName} (
62
+ saga_descriptor TEXT NOT NULL,
63
+ origin_id TEXT NOT NULL,
64
+ event_id TEXT NOT NULL,
65
+ PRIMARY KEY (saga_descriptor, origin_id, event_id),
66
+ FOREIGN KEY (event_id) REFERENCES ${this.#eventsTableName}(id) ON DELETE CASCADE
67
+ )
68
+ `);
69
+ const eventsIndexPrefix = indexPrefix(this.#eventsTableName);
70
+ const sagasIndexPrefix = indexPrefix(this.#eventSagasTableName);
71
+ await db.query(`
72
+ CREATE INDEX IF NOT EXISTS "${eventsIndexPrefix}_aggregate_id_idx"
73
+ ON ${this.#eventsTableName} (aggregate_id, aggregate_version)
74
+ `);
75
+ // Only checked rows participate in aggregate-version uniqueness, so ignoreConcurrencyError can opt out.
76
+ await db.query(`
77
+ CREATE UNIQUE INDEX IF NOT EXISTS "${this.#aggregateVersionIndexName}"
78
+ ON ${this.#eventsTableName} (aggregate_id, aggregate_version)
79
+ WHERE check_concurrency
80
+ AND aggregate_id IS NOT NULL
81
+ AND aggregate_version IS NOT NULL
82
+ `);
83
+ await db.query(`
84
+ CREATE INDEX IF NOT EXISTS "${eventsIndexPrefix}_type_position_idx"
85
+ ON ${this.#eventsTableName} (type, position)
86
+ `);
87
+ await db.query(`
88
+ CREATE INDEX IF NOT EXISTS "${sagasIndexPrefix}_event_id_idx"
89
+ ON ${this.#eventSagasTableName} (event_id)
90
+ `);
91
+ }
92
+ // eslint-disable-next-line class-methods-use-this
93
+ getNewId() {
94
+ return randomUUID().replaceAll('-', '');
95
+ }
96
+ async commitEvents(events, options) {
97
+ try {
98
+ await this.runInTransaction(async () => {
99
+ const metaJson = options?.meta ? JSON.stringify(options.meta) : null;
100
+ for (const event of events) {
101
+ const { sagaOrigins, id, ...eventData } = event;
102
+ const eventId = id ?? this.getNewId();
103
+ assertString(eventId, 'event.id');
104
+ event.id = eventId;
105
+ await this.connection.query(`
106
+ INSERT INTO ${this.#eventsTableName} (
107
+ id,
108
+ aggregate_id,
109
+ aggregate_version,
110
+ type,
111
+ data,
112
+ meta,
113
+ check_concurrency
114
+ )
115
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7)
116
+ `, [
117
+ eventId,
118
+ event.aggregateId !== undefined ? String(event.aggregateId) : null,
119
+ event.aggregateVersion ?? null,
120
+ event.type,
121
+ JSON.stringify(eventData),
122
+ metaJson,
123
+ !options?.ignoreConcurrencyError
124
+ ]);
125
+ if (sagaOrigins) {
126
+ for (const [descriptor, originId] of Object.entries(sagaOrigins)) {
127
+ await this.connection.query(`
128
+ INSERT INTO ${this.#eventSagasTableName} (
129
+ saga_descriptor,
130
+ origin_id,
131
+ event_id
132
+ )
133
+ VALUES ($1, $2, $3)
134
+ `, [descriptor, String(originId), eventId]);
135
+ }
136
+ }
137
+ }
138
+ });
139
+ }
140
+ catch (error) {
141
+ if (this.isAggregateVersionUniqueViolation(error))
142
+ throw new ConcurrencyError('Concurrency conflict: duplicate event version', { cause: error });
143
+ throw error;
144
+ }
145
+ return events;
146
+ }
147
+ async *getAggregateEvents(aggregateId, options) {
148
+ await this.assertConnection();
149
+ const rows = await this.connection.query(`
150
+ WITH tail AS (
151
+ SELECT id AS tail_id
152
+ FROM ${this.#eventsTableName}
153
+ WHERE aggregate_id = $1
154
+ AND ($2::integer IS NULL OR aggregate_version > $2)
155
+ ORDER BY position DESC
156
+ LIMIT 1
157
+ )
158
+ SELECT
159
+ e.id,
160
+ e.aggregate_id,
161
+ e.aggregate_version,
162
+ e.type,
163
+ e.data,
164
+ e.meta,
165
+ e.position,
166
+ jsonb_object_agg(sr.saga_descriptor, sr.origin_id)
167
+ FILTER (WHERE sr.saga_descriptor IS NOT NULL) AS saga_origins
168
+ FROM ${this.#eventsTableName} e
169
+ LEFT JOIN tail ON TRUE
170
+ LEFT JOIN ${this.#eventSagasTableName} sr
171
+ ON sr.event_id = e.id
172
+ WHERE e.aggregate_id = $1
173
+ AND ($2::integer IS NULL OR e.aggregate_version > $2)
174
+ AND (
175
+ $3::text[] IS NULL
176
+ OR e.type = ANY($3::text[])
177
+ OR ($4 = 'last' AND e.id = tail.tail_id)
178
+ )
179
+ GROUP BY e.id, e.aggregate_id, e.aggregate_version, e.type, e.data, e.meta, e.position
180
+ ORDER BY e.position
181
+ `, [
182
+ String(aggregateId),
183
+ options?.snapshot?.aggregateVersion ?? null,
184
+ options?.eventTypes ?? null,
185
+ options?.tail ?? null
186
+ ]);
187
+ for (const row of rows.rows)
188
+ yield reconstructEvent(row);
189
+ }
190
+ async *getSagaEvents(sagaId, { beforeEvent }) {
191
+ await this.assertConnection();
192
+ assertString(beforeEvent?.id, 'beforeEvent.id');
193
+ const { sagaDescriptor, originEventId } = parseSagaId(sagaId);
194
+ if (beforeEvent.sagaOrigins?.[sagaDescriptor] !== originEventId)
195
+ throw new TypeError('beforeEvent.sagaOrigins does not match sagaId');
196
+ const rows = await this.connection.query(`
197
+ WITH bounds AS (
198
+ SELECT
199
+ (SELECT position FROM ${this.#eventsTableName} WHERE id = $2) AS origin_position,
200
+ (SELECT position FROM ${this.#eventsTableName} WHERE id = $3) AS before_position
201
+ )
202
+ SELECT
203
+ b.origin_position,
204
+ b.before_position,
205
+ e.id,
206
+ e.aggregate_id,
207
+ e.aggregate_version,
208
+ e.type,
209
+ e.data,
210
+ e.meta,
211
+ e.position,
212
+ e.saga_origins
213
+ FROM bounds b
214
+ LEFT JOIN LATERAL (
215
+ SELECT
216
+ e.id,
217
+ e.aggregate_id,
218
+ e.aggregate_version,
219
+ e.type,
220
+ e.data,
221
+ e.meta,
222
+ e.position,
223
+ jsonb_object_agg(sr.saga_descriptor, sr.origin_id)
224
+ FILTER (WHERE sr.saga_descriptor IS NOT NULL) AS saga_origins
225
+ FROM ${this.#eventsTableName} e
226
+ LEFT JOIN ${this.#eventSagasTableName} filter_sr
227
+ ON filter_sr.event_id = e.id
228
+ AND filter_sr.saga_descriptor = $1
229
+ AND filter_sr.origin_id = $2
230
+ LEFT JOIN ${this.#eventSagasTableName} sr
231
+ ON sr.event_id = e.id
232
+ WHERE b.origin_position IS NOT NULL
233
+ AND b.before_position IS NOT NULL
234
+ AND e.position >= b.origin_position
235
+ AND e.position < b.before_position
236
+ AND (e.id = $2 OR filter_sr.event_id IS NOT NULL)
237
+ GROUP BY e.id, e.aggregate_id, e.aggregate_version, e.type, e.data, e.meta, e.position
238
+ ORDER BY e.position
239
+ ) e ON TRUE
240
+ ORDER BY e.position
241
+ `, [sagaDescriptor, originEventId, beforeEvent.id]);
242
+ const bounds = rows.rows[0];
243
+ if (!bounds?.origin_position)
244
+ throw new Error(`origin event ${originEventId} not found`);
245
+ if (!bounds.before_position)
246
+ throw new Error(`beforeEvent ${beforeEvent.id} not found`);
247
+ for (const row of rows.rows) {
248
+ if (row.id)
249
+ yield reconstructEvent(row);
250
+ }
251
+ }
252
+ async *getEventsByTypes(eventTypes, options) {
253
+ await this.assertConnection();
254
+ const lastEventId = options?.afterEvent?.id;
255
+ if (options?.afterEvent)
256
+ assertString(options.afterEvent.id, 'options.afterEvent.id');
257
+ let afterPosition = 0;
258
+ if (lastEventId) {
259
+ const row = await this.connection.query(`
260
+ SELECT position
261
+ FROM ${this.#eventsTableName}
262
+ WHERE id = $1
263
+ `, [lastEventId]);
264
+ if (!row.rows.length)
265
+ return;
266
+ afterPosition = row.rows[0].position;
267
+ }
268
+ const rows = await this.connection.query(`
269
+ SELECT
270
+ e.id,
271
+ e.aggregate_id,
272
+ e.aggregate_version,
273
+ e.type,
274
+ e.data,
275
+ e.meta,
276
+ e.position,
277
+ jsonb_object_agg(sr.saga_descriptor, sr.origin_id)
278
+ FILTER (WHERE sr.saga_descriptor IS NOT NULL) AS saga_origins
279
+ FROM ${this.#eventsTableName} e
280
+ LEFT JOIN ${this.#eventSagasTableName} sr
281
+ ON sr.event_id = e.id
282
+ WHERE e.position > $1
283
+ AND e.type = ANY($2::text[])
284
+ GROUP BY e.id, e.aggregate_id, e.aggregate_version, e.type, e.data, e.meta, e.position
285
+ ORDER BY e.position
286
+ `, [afterPosition, eventTypes]);
287
+ for (const row of rows.rows)
288
+ yield reconstructEvent(row);
289
+ }
290
+ async process(batch) {
291
+ const events = [];
292
+ for (const item of batch) {
293
+ if (!item.event)
294
+ throw new Error('Event batch does not contain `event`');
295
+ events.push(item.event);
296
+ }
297
+ await this.commitEvents(events, {
298
+ ignoreConcurrencyError: batch[0]?.ignoreConcurrencyError,
299
+ meta: extractMeta(batch[0])
300
+ });
301
+ return batch;
302
+ }
303
+ isAggregateVersionUniqueViolation(error) {
304
+ return typeof error === 'object' &&
305
+ error !== null &&
306
+ 'code' in error &&
307
+ error.code === '23505' &&
308
+ error.constraint === this.#aggregateVersionIndexName;
309
+ }
310
+ }
311
+ //# sourceMappingURL=PostgresqlEventStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostgresqlEventStorage.js","sourceRoot":"","sources":["../../../src/postgresql/PostgresqlEventStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ import { assertDefined, assertFunction, assertNonNegativeInteger, assertString } from "../utils/assert.js";
2
+ import { AbstractPostgresqlAccessor } from "./AbstractPostgresqlAccessor.js";
3
+ import { quoteIdentifier } from "./utils/index.js";
4
+ /**
5
+ * PostgreSQL-backed implementation of IObjectStorage.
6
+ *
7
+ * Each record is stored as a row `{ id, data, version }`.
8
+ * The version field enables optimistic concurrency control: `update` and
9
+ * `updateEnforcingNew` re-read the record after the user callback runs and
10
+ * atomically commit only when the version still matches.
11
+ * On mismatch the operation retries up to `maxRetries` times.
12
+ */
13
+ export class PostgresqlObjectStorage extends AbstractPostgresqlAccessor {
14
+ #tableName;
15
+ #maxRetries;
16
+ constructor(o) {
17
+ super(o);
18
+ assertString(o.tableName, 'tableName');
19
+ if (o.maxRetries !== undefined)
20
+ assertNonNegativeInteger(o.maxRetries, 'maxRetries');
21
+ this.#tableName = quoteIdentifier(o.tableName);
22
+ this.#maxRetries = o.maxRetries ?? 100;
23
+ }
24
+ async initialize(db) {
25
+ await db.query(`
26
+ CREATE TABLE IF NOT EXISTS ${this.#tableName} (
27
+ id text PRIMARY KEY,
28
+ version integer NOT NULL DEFAULT 1,
29
+ data jsonb NOT NULL
30
+ )
31
+ `);
32
+ }
33
+ async get(id) {
34
+ assertDefined(id, 'id');
35
+ await this.assertConnection();
36
+ const result = await this.connection.query(`
37
+ SELECT data, version
38
+ FROM ${this.#tableName}
39
+ WHERE id = $1
40
+ `, [String(id)]);
41
+ const row = result.rows[0];
42
+ if (!row)
43
+ return undefined;
44
+ return PostgresqlObjectStorage.parseData(row.data);
45
+ }
46
+ async create(id, data) {
47
+ assertDefined(id, 'id');
48
+ await this.assertConnection();
49
+ const result = await this.connection.query(`
50
+ INSERT INTO ${this.#tableName} (id, data)
51
+ VALUES ($1, $2::jsonb)
52
+ ON CONFLICT (id) DO NOTHING
53
+ `, [String(id), JSON.stringify(data)]);
54
+ if (result.rowCount !== 1)
55
+ throw new Error(`Record '${id}' could not be created`);
56
+ }
57
+ async update(id, update) {
58
+ assertDefined(id, 'id');
59
+ assertFunction(update, 'update');
60
+ await this.assertConnection();
61
+ for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
62
+ const row = await this.getRow(id);
63
+ if (!row)
64
+ throw new Error(`Record '${id}' does not exist`);
65
+ const updatedData = update(PostgresqlObjectStorage.parseData(row.data));
66
+ const result = await this.updateByVersion(id, row.version, updatedData);
67
+ if (result.rowCount === 1)
68
+ return;
69
+ // version mismatch - retry
70
+ }
71
+ throw new Error(`Record '${id}' could not be updated after ${this.#maxRetries} retries`);
72
+ }
73
+ async updateEnforcingNew(id, update) {
74
+ assertDefined(id, 'id');
75
+ assertFunction(update, 'update');
76
+ await this.assertConnection();
77
+ for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
78
+ const row = await this.getRow(id);
79
+ if (row) {
80
+ const updatedData = update(PostgresqlObjectStorage.parseData(row.data));
81
+ const result = await this.updateByVersion(id, row.version, updatedData);
82
+ if (result.rowCount === 1)
83
+ return;
84
+ // version mismatch - retry
85
+ }
86
+ else {
87
+ const result = await this.connection.query(`
88
+ INSERT INTO ${this.#tableName} (id, data)
89
+ VALUES ($1, $2::jsonb)
90
+ ON CONFLICT (id) DO NOTHING
91
+ `, [String(id), JSON.stringify(update(undefined))]);
92
+ if (result.rowCount === 1)
93
+ return;
94
+ // Another process inserted first - retry
95
+ }
96
+ }
97
+ throw new Error(`Record '${id}' could not be upserted after ${this.#maxRetries} retries`);
98
+ }
99
+ async delete(id) {
100
+ assertDefined(id, 'id');
101
+ await this.assertConnection();
102
+ const result = await this.connection.query(`
103
+ DELETE FROM ${this.#tableName}
104
+ WHERE id = $1
105
+ `, [String(id)]);
106
+ return result.rowCount === 1;
107
+ }
108
+ async getRow(id) {
109
+ const result = await this.connection.query(`
110
+ SELECT data, version
111
+ FROM ${this.#tableName}
112
+ WHERE id = $1
113
+ `, [String(id)]);
114
+ return result.rows[0];
115
+ }
116
+ updateByVersion(id, version, data) {
117
+ return this.connection.query(`
118
+ UPDATE ${this.#tableName}
119
+ SET
120
+ data = $1::jsonb,
121
+ version = version + 1
122
+ WHERE
123
+ id = $2
124
+ AND version = $3
125
+ `, [JSON.stringify(data), String(id), version]);
126
+ }
127
+ static parseData(data) {
128
+ if (typeof data === 'string')
129
+ return JSON.parse(data);
130
+ return data;
131
+ }
132
+ }
133
+ //# sourceMappingURL=PostgresqlObjectStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostgresqlObjectStorage.js","sourceRoot":"","sources":["../../../src/postgresql/PostgresqlObjectStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { assertString } from "../utils/assert.js";
2
+ import { AbstractPostgresqlView } from "./AbstractPostgresqlView.js";
3
+ import { PostgresqlObjectStorage } from "./PostgresqlObjectStorage.js";
4
+ /**
5
+ * PostgreSQL-backed object view with restore locking and last-processed-event tracking.
6
+ */
7
+ export class PostgresqlObjectView extends AbstractPostgresqlView {
8
+ #postgresqlObjectStorage;
9
+ constructor(options) {
10
+ assertString(options?.tableNamePrefix, 'tableNamePrefix');
11
+ assertString(options?.schemaVersion, 'schemaVersion');
12
+ super(options);
13
+ this.#postgresqlObjectStorage = new PostgresqlObjectStorage({
14
+ viewModelPostgresqlDb: options.viewModelPostgresqlDb,
15
+ viewModelPostgresqlDbFactory: options.viewModelPostgresqlDbFactory,
16
+ tableName: `${options.tableNamePrefix}_${options.schemaVersion}`,
17
+ maxRetries: options.postgresqlObjectStorageMaxRetries
18
+ });
19
+ }
20
+ // eslint-disable-next-line class-methods-use-this
21
+ initialize(_db) {
22
+ // No need to initialize the table here, it's done in PostgresqlObjectStorage
23
+ }
24
+ async get(id) {
25
+ if (!this.ready)
26
+ await this.once('ready');
27
+ return this.#postgresqlObjectStorage.get(id);
28
+ }
29
+ async create(id, data) {
30
+ await this.#postgresqlObjectStorage.create(id, data);
31
+ }
32
+ async update(id, update) {
33
+ await this.#postgresqlObjectStorage.update(id, update);
34
+ }
35
+ async updateEnforcingNew(id, update) {
36
+ await this.#postgresqlObjectStorage.updateEnforcingNew(id, update);
37
+ }
38
+ async delete(id) {
39
+ return this.#postgresqlObjectStorage.delete(id);
40
+ }
41
+ }
42
+ //# sourceMappingURL=PostgresqlObjectView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostgresqlObjectView.js","sourceRoot":"","sources":["../../../src/postgresql/PostgresqlObjectView.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAGvE;;GAEG;AACH,MAAM,OAAO,oBACZ,SAAQ,sBAAsB;IAGrB,wBAAwB,CAAmC;IAEpE,YAAY,OAGX;QACA,YAAY,CAAC,OAAO,EAAE,eAAe,EAAE,iBAAiB,CAAC,CAAC;QAC1D,YAAY,CAAC,OAAO,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC;QAEtD,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,wBAAwB,GAAG,IAAI,uBAAuB,CAAU;YACpE,qBAAqB,EAAE,OAAO,CAAC,qBAAqB;YACpD,4BAA4B,EAAE,OAAO,CAAC,4BAA4B;YAClE,SAAS,EAAE,GAAG,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,aAAa,EAAE;YAChE,UAAU,EAAE,OAAO,CAAC,iCAAiC;SACrD,CAAC,CAAC;IACJ,CAAC;IAED,kDAAkD;IACxC,UAAU,CAAC,GAAyB;QAC7C,6EAA6E;IAC9E,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAc;QACvB,IAAI,CAAC,IAAI,CAAC,KAAK;YACd,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAE1B,OAAO,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAc,EAAE,IAAa;QACzC,MAAM,IAAI,CAAC,wBAAwB,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAc,EAAE,MAA+B;QAC3D,MAAM,IAAI,CAAC,wBAAwB,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,EAAc,EAAE,MAAgC;QACxE,MAAM,IAAI,CAAC,wBAAwB,CAAC,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAc;QAC1B,OAAO,IAAI,CAAC,wBAAwB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;CACD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PostgresqlProjectionDataParams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostgresqlProjectionDataParams.js","sourceRoot":"","sources":["../../../src/postgresql/PostgresqlProjectionDataParams.ts"],"names":[],"mappings":""}