@event-driven-io/emmett-postgresql 0.43.0-beta.1 → 0.43.0-beta.10

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/dist/index.js CHANGED
@@ -48,14 +48,26 @@ var ConcurrencyError = class _ConcurrencyError extends EmmettError {
48
48
  };
49
49
 
50
50
  // ../emmett/dist/index.js
51
- import { v4 as uuid4 } from "uuid";
51
+ import { v4 as uuid5 } from "uuid";
52
+ import { v7 as uuid2 } from "uuid";
52
53
  import { v7 as uuid } from "uuid";
53
54
  import retry from "async-retry";
54
- import { v7 as uuid2 } from "uuid";
55
- import { v4 as uuid3 } from "uuid";
55
+ import { v7 as uuid3 } from "uuid";
56
+ import { v4 as uuid4 } from "uuid";
57
+ import { v7 as uuid6 } from "uuid";
56
58
  var emmettPrefix = "emt";
57
59
  var defaultTag = `${emmettPrefix}:default`;
58
60
  var unknownTag = `${emmettPrefix}:unknown`;
61
+ var canCreateEventStoreSession = (eventStore) => "withSession" in eventStore;
62
+ var nulloSessionFactory = (eventStore) => ({
63
+ withSession: (callback) => {
64
+ const nulloSession = {
65
+ eventStore,
66
+ close: () => Promise.resolve()
67
+ };
68
+ return callback(nulloSession);
69
+ }
70
+ });
59
71
  var STREAM_EXISTS = "STREAM_EXISTS";
60
72
  var STREAM_DOES_NOT_EXIST = "STREAM_DOES_NOT_EXIST";
61
73
  var NO_CONCURRENCY_CHECK = "NO_CONCURRENCY_CHECK";
@@ -76,6 +88,10 @@ var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends C
76
88
  Object.setPrototypeOf(this, _ExpectedVersionConflictError.prototype);
77
89
  }
78
90
  };
91
+ var isExpectedVersionConflictError = (error) => error instanceof ExpectedVersionConflictError || EmmettError.isInstanceOf(
92
+ error,
93
+ ExpectedVersionConflictError.Codes.ConcurrencyError
94
+ );
79
95
  var isPrimitive = (value) => {
80
96
  const type = typeof value;
81
97
  return value === null || value === void 0 || type === "boolean" || type === "number" || type === "string" || type === "symbol" || type === "bigint";
@@ -281,27 +297,110 @@ var toNormalizedString = (value) => value.toString().padStart(19, "0");
281
297
  var bigInt = {
282
298
  toNormalizedString
283
299
  };
284
- var ParseError = class extends Error {
285
- constructor(text) {
286
- super(`Cannot parse! ${text}`);
300
+ var bigIntReplacer = (_key, value) => {
301
+ return typeof value === "bigint" ? value.toString() : value;
302
+ };
303
+ var dateReplacer = (_key, value) => {
304
+ return value instanceof Date ? value.toISOString() : value;
305
+ };
306
+ var isFirstLetterNumeric = (str) => {
307
+ const c = str.charCodeAt(0);
308
+ return c >= 48 && c <= 57;
309
+ };
310
+ var isFirstLetterNumericOrMinus = (str) => {
311
+ const c = str.charCodeAt(0);
312
+ return c >= 48 && c <= 57 || c === 45;
313
+ };
314
+ var bigIntReviver = (_key, value, context) => {
315
+ if (typeof value === "number" && Number.isInteger(value) && !Number.isSafeInteger(value)) {
316
+ try {
317
+ return BigInt(context?.source ?? value.toString());
318
+ } catch {
319
+ return value;
320
+ }
321
+ }
322
+ if (typeof value === "string" && value.length > 15) {
323
+ if (isFirstLetterNumericOrMinus(value)) {
324
+ const num = Number(value);
325
+ if (Number.isFinite(num) && !Number.isSafeInteger(num)) {
326
+ try {
327
+ return BigInt(value);
328
+ } catch {
329
+ }
330
+ }
331
+ }
332
+ }
333
+ return value;
334
+ };
335
+ var dateReviver = (_key, value) => {
336
+ if (typeof value === "string" && value.length === 24 && isFirstLetterNumeric(value) && value[10] === "T" && value[23] === "Z") {
337
+ const date = new Date(value);
338
+ if (!isNaN(date.getTime())) {
339
+ return date;
340
+ }
287
341
  }
342
+ return value;
288
343
  };
289
- var JSONParser = {
290
- stringify: (value, options) => {
291
- return JSON.stringify(
292
- options?.map ? options.map(value) : value,
293
- //TODO: Consider adding support to DateTime and adding specific format to mark that's a bigint
344
+ var composeJSONReplacers = (...replacers) => {
345
+ const filteredReplacers = replacers.filter((r) => r !== void 0);
346
+ if (filteredReplacers.length === 0) return void 0;
347
+ return (key, value) => (
348
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
349
+ filteredReplacers.reduce(
294
350
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
295
- (_, v) => typeof v === "bigint" ? v.toString() : v
296
- );
297
- },
298
- parse: (text, options) => {
299
- const parsed = JSON.parse(text, options?.reviver);
300
- if (options?.typeCheck && !options?.typeCheck(parsed))
301
- throw new ParseError(text);
302
- return options?.map ? options.map(parsed) : parsed;
303
- }
351
+ (accValue, replacer) => replacer(key, accValue),
352
+ value
353
+ )
354
+ );
355
+ };
356
+ var composeJSONRevivers = (...revivers) => {
357
+ const filteredRevivers = revivers.filter((r) => r !== void 0);
358
+ if (filteredRevivers.length === 0) return void 0;
359
+ return (key, value, context) => (
360
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
361
+ filteredRevivers.reduce(
362
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
363
+ (accValue, reviver) => reviver(key, accValue, context),
364
+ value
365
+ )
366
+ );
367
+ };
368
+ var JSONReplacer = (opts) => composeJSONReplacers(
369
+ opts?.replacer,
370
+ opts?.failOnBigIntSerialization !== true ? JSONReplacers.bigInt : void 0,
371
+ opts?.useDefaultDateSerialization !== true ? JSONReplacers.date : void 0
372
+ );
373
+ var JSONReviver = (opts) => composeJSONRevivers(
374
+ opts?.reviver,
375
+ opts?.parseBigInts === true ? JSONRevivers.bigInt : void 0,
376
+ opts?.parseDates === true ? JSONRevivers.date : void 0
377
+ );
378
+ var JSONReplacers = {
379
+ bigInt: bigIntReplacer,
380
+ date: dateReplacer
381
+ };
382
+ var JSONRevivers = {
383
+ bigInt: bigIntReviver,
384
+ date: dateReviver
385
+ };
386
+ var jsonSerializer = (options) => {
387
+ const defaultReplacer = JSONReplacer(options);
388
+ const defaultReviver = JSONReviver(options);
389
+ return {
390
+ serialize: (object, serializerOptions) => JSON.stringify(
391
+ object,
392
+ serializerOptions ? JSONReplacer(serializerOptions) : defaultReplacer
393
+ ),
394
+ deserialize: (payload, deserializerOptions) => JSON.parse(
395
+ payload,
396
+ deserializerOptions ? JSONReviver(deserializerOptions) : defaultReviver
397
+ )
398
+ };
304
399
  };
400
+ var JSONSerializer = Object.assign(jsonSerializer(), {
401
+ from: (options) => options?.serialization?.serializer ?? (options?.serialization?.options ? jsonSerializer(options?.serialization?.options) : JSONSerializer)
402
+ });
403
+ var NoRetries = { retries: 0 };
305
404
  var asyncRetry = async (fn, opts) => {
306
405
  if (opts === void 0 || opts.retries === 0) return fn();
307
406
  return retry(
@@ -310,16 +409,16 @@ var asyncRetry = async (fn, opts) => {
310
409
  const result = await fn();
311
410
  if (opts?.shouldRetryResult && opts.shouldRetryResult(result)) {
312
411
  throw new EmmettError(
313
- `Retrying because of result: ${JSONParser.stringify(result)}`
412
+ `Retrying because of result: ${JSONSerializer.serialize(result)}`
314
413
  );
315
414
  }
316
415
  return result;
317
- } catch (error2) {
318
- if (opts?.shouldRetryError && !opts.shouldRetryError(error2)) {
319
- bail(error2);
416
+ } catch (error) {
417
+ if (opts?.shouldRetryError && !opts.shouldRetryError(error)) {
418
+ bail(error);
320
419
  return void 0;
321
420
  }
322
- throw error2;
421
+ throw error;
323
422
  }
324
423
  },
325
424
  opts ?? { retries: 0 }
@@ -376,7 +475,7 @@ var bigIntProcessorCheckpoint = (value) => bigInt.toNormalizedString(value);
376
475
  var parseBigIntProcessorCheckpoint = (value) => BigInt(value);
377
476
  var defaultProcessorVersion = 1;
378
477
  var defaultProcessorPartition = defaultTag;
379
- var getProcessorInstanceId = (processorId) => `${processorId}:${uuid2()}`;
478
+ var getProcessorInstanceId = (processorId) => `${processorId}:${uuid3()}`;
380
479
  var getProjectorId = (options) => `emt:processor:projector:${options.projectionName}`;
381
480
  var reactor = (options) => {
382
481
  const {
@@ -423,12 +522,13 @@ var reactor = (options) => {
423
522
  id: processorId,
424
523
  instanceId,
425
524
  type,
525
+ canHandle,
426
526
  init,
427
527
  start: async (startOptions) => {
428
528
  if (isActive) return;
429
529
  await init(startOptions);
430
530
  isActive = true;
431
- closeSignal = onShutdown(() => close({}));
531
+ closeSignal = onShutdown(() => close(startOptions));
432
532
  if (lastCheckpoint !== null)
433
533
  return {
434
534
  lastCheckpoint
@@ -555,9 +655,9 @@ var assertDeepEqual = (actual, expected, message2) => {
555
655
  if (!deepEquals(actual, expected))
556
656
  throw new AssertionError(
557
657
  message2 ?? `subObj:
558
- ${JSONParser.stringify(expected)}
658
+ ${JSONSerializer.serialize(expected)}
559
659
  is not equal to
560
- ${JSONParser.stringify(actual)}`
660
+ ${JSONSerializer.serialize(actual)}`
561
661
  );
562
662
  };
563
663
  function assertTrue(condition, message2) {
@@ -571,26 +671,29 @@ function assertEqual(expected, actual, message2) {
571
671
  if (expected !== actual)
572
672
  throw new AssertionError(
573
673
  `${message2 ?? "Objects are not equal"}:
574
- Expected: ${JSONParser.stringify(expected)}
575
- Actual: ${JSONParser.stringify(actual)}`
674
+ Expected: ${JSONSerializer.serialize(expected)}
675
+ Actual: ${JSONSerializer.serialize(actual)}`
576
676
  );
577
677
  }
578
678
  function assertNotEqual(obj, other, message2) {
579
679
  if (obj === other)
580
680
  throw new AssertionError(
581
- message2 ?? `Objects are equal: ${JSONParser.stringify(obj)}`
681
+ message2 ?? `Objects are equal: ${JSONSerializer.serialize(obj)}`
582
682
  );
583
683
  }
584
684
  function assertIsNotNull(result) {
585
685
  assertNotEqual(result, null);
586
686
  assertOk(result);
587
687
  }
688
+ function assertIsNull(result) {
689
+ assertEqual(result, null);
690
+ }
588
691
  var assertThatArray = (array) => {
589
692
  return {
590
693
  isEmpty: () => assertEqual(
591
694
  array.length,
592
695
  0,
593
- `Array is not empty ${JSONParser.stringify(array)}`
696
+ `Array is not empty ${JSONSerializer.serialize(array)}`
594
697
  ),
595
698
  isNotEmpty: () => assertNotEqual(array.length, 0, `Array is empty`),
596
699
  hasSize: (length) => assertEqual(array.length, length),
@@ -690,6 +793,281 @@ var upcastRecordedMessage = (recordedMessage, options) => {
690
793
  };
691
794
  };
692
795
  var projection = (definition) => definition;
796
+ var WorkflowHandlerStreamVersionConflictRetryOptions = {
797
+ retries: 3,
798
+ minTimeout: 100,
799
+ factor: 1.5,
800
+ shouldRetryError: isExpectedVersionConflictError
801
+ };
802
+ var fromWorkflowHandlerRetryOptions = (retryOptions) => {
803
+ if (retryOptions === void 0) return NoRetries;
804
+ if ("onVersionConflict" in retryOptions) {
805
+ if (typeof retryOptions.onVersionConflict === "boolean")
806
+ return WorkflowHandlerStreamVersionConflictRetryOptions;
807
+ else if (typeof retryOptions.onVersionConflict === "number")
808
+ return {
809
+ ...WorkflowHandlerStreamVersionConflictRetryOptions,
810
+ retries: retryOptions.onVersionConflict
811
+ };
812
+ else return retryOptions.onVersionConflict;
813
+ }
814
+ return retryOptions;
815
+ };
816
+ var emptyHandlerResult = (nextExpectedStreamVersion = 0n) => ({
817
+ newMessages: [],
818
+ createdNewStream: false,
819
+ nextExpectedStreamVersion
820
+ });
821
+ var createInputMetadata = (originalMessageId, action) => ({
822
+ originalMessageId,
823
+ input: true,
824
+ action
825
+ });
826
+ var tagOutputMessage = (msg, action) => {
827
+ const existingMetadata = "metadata" in msg && msg.metadata ? msg.metadata : {};
828
+ return {
829
+ ...msg,
830
+ metadata: {
831
+ ...existingMetadata,
832
+ action
833
+ }
834
+ };
835
+ };
836
+ var createWrappedInitialState = (initialState) => {
837
+ return () => ({
838
+ userState: initialState(),
839
+ processedInputIds: /* @__PURE__ */ new Set()
840
+ });
841
+ };
842
+ var createWrappedEvolve = (evolve, workflowName, separateInputInboxFromProcessing) => {
843
+ return (state, event2) => {
844
+ const metadata = event2.metadata;
845
+ let processedInputIds = state.processedInputIds;
846
+ if (metadata?.input === true && typeof metadata?.originalMessageId === "string") {
847
+ processedInputIds = new Set(state.processedInputIds);
848
+ processedInputIds.add(metadata.originalMessageId);
849
+ }
850
+ if (separateInputInboxFromProcessing && metadata?.input === true) {
851
+ return {
852
+ userState: state.userState,
853
+ processedInputIds
854
+ };
855
+ }
856
+ const eventType = event2.type;
857
+ const eventForEvolve = eventType.startsWith(`${workflowName}:`) ? {
858
+ ...event2,
859
+ type: eventType.replace(`${workflowName}:`, "")
860
+ } : event2;
861
+ return {
862
+ userState: evolve(state.userState, eventForEvolve),
863
+ processedInputIds
864
+ };
865
+ };
866
+ };
867
+ var workflowStreamName = ({
868
+ workflowName,
869
+ workflowId
870
+ }) => `emt:workflow:${workflowName}:${workflowId}`;
871
+ var WorkflowHandler = (options) => async (store, message2, handleOptions) => asyncRetry(
872
+ async () => {
873
+ const result = await withSession2(store, async ({ eventStore }) => {
874
+ const {
875
+ workflow: { evolve, initialState, decide, name: workflowName },
876
+ getWorkflowId: getWorkflowId2
877
+ } = options;
878
+ const inputMessageId = (
879
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
880
+ ("metadata" in message2 && message2.metadata?.messageId ? (
881
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
882
+ message2.metadata.messageId
883
+ ) : void 0) ?? uuid6()
884
+ );
885
+ const messageWithMetadata = {
886
+ ...message2,
887
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
888
+ metadata: {
889
+ messageId: inputMessageId,
890
+ ...message2.metadata
891
+ }
892
+ };
893
+ const workflowId = getWorkflowId2(messageWithMetadata);
894
+ if (!workflowId) {
895
+ return emptyHandlerResult();
896
+ }
897
+ const streamName = options.mapWorkflowId ? options.mapWorkflowId(workflowId) : workflowStreamName({ workflowName, workflowId });
898
+ const messageType = messageWithMetadata.type;
899
+ const hasWorkflowPrefix = messageType.startsWith(`${workflowName}:`);
900
+ if (options.separateInputInboxFromProcessing && !hasWorkflowPrefix) {
901
+ const inputMetadata2 = createInputMetadata(
902
+ inputMessageId,
903
+ "InitiatedBy"
904
+ );
905
+ const inputToStore2 = {
906
+ type: `${workflowName}:${messageWithMetadata.type}`,
907
+ data: messageWithMetadata.data,
908
+ kind: messageWithMetadata.kind,
909
+ metadata: inputMetadata2
910
+ };
911
+ const appendResult2 = await eventStore.appendToStream(
912
+ streamName,
913
+ [inputToStore2],
914
+ {
915
+ ...handleOptions,
916
+ expectedStreamVersion: handleOptions?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK
917
+ }
918
+ );
919
+ return {
920
+ ...appendResult2,
921
+ newMessages: []
922
+ };
923
+ }
924
+ const wrappedInitialState = createWrappedInitialState(initialState);
925
+ const wrappedEvolve = createWrappedEvolve(
926
+ evolve,
927
+ workflowName,
928
+ options.separateInputInboxFromProcessing ?? false
929
+ );
930
+ const aggregationResult = await eventStore.aggregateStream(streamName, {
931
+ evolve: wrappedEvolve,
932
+ initialState: wrappedInitialState,
933
+ read: {
934
+ ...handleOptions,
935
+ // expected stream version is passed to fail fast
936
+ // if stream is in the wrong state
937
+ expectedStreamVersion: handleOptions?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK
938
+ }
939
+ });
940
+ const { currentStreamVersion } = aggregationResult;
941
+ const { userState: state, processedInputIds } = aggregationResult.state;
942
+ if (processedInputIds.has(inputMessageId)) {
943
+ return emptyHandlerResult(currentStreamVersion);
944
+ }
945
+ const messageForDecide = hasWorkflowPrefix ? {
946
+ ...messageWithMetadata,
947
+ type: messageType.replace(`${workflowName}:`, "")
948
+ } : messageWithMetadata;
949
+ const result2 = decide(messageForDecide, state);
950
+ const inputMetadata = createInputMetadata(
951
+ inputMessageId,
952
+ aggregationResult.streamExists ? "Received" : "InitiatedBy"
953
+ );
954
+ const inputToStore = {
955
+ type: `${workflowName}:${messageWithMetadata.type}`,
956
+ data: messageWithMetadata.data,
957
+ kind: messageWithMetadata.kind,
958
+ metadata: inputMetadata
959
+ };
960
+ const outputMessages = (Array.isArray(result2) ? result2 : [result2]).filter((msg) => msg !== void 0 && msg !== null);
961
+ const outputCommandTypes = options.outputs?.commands ?? [];
962
+ const taggedOutputMessages = outputMessages.map((msg) => {
963
+ const action = outputCommandTypes.includes(
964
+ msg.type
965
+ ) ? "Sent" : "Published";
966
+ return tagOutputMessage(msg, action);
967
+ });
968
+ const messagesToAppend = options.separateInputInboxFromProcessing && hasWorkflowPrefix ? [...taggedOutputMessages] : [inputToStore, ...taggedOutputMessages];
969
+ if (messagesToAppend.length === 0) {
970
+ return emptyHandlerResult(currentStreamVersion);
971
+ }
972
+ const expectedStreamVersion = handleOptions?.expectedStreamVersion ?? (aggregationResult.streamExists ? currentStreamVersion : STREAM_DOES_NOT_EXIST);
973
+ const appendResult = await eventStore.appendToStream(
974
+ streamName,
975
+ // TODO: Fix this cast
976
+ messagesToAppend,
977
+ {
978
+ ...handleOptions,
979
+ expectedStreamVersion
980
+ }
981
+ );
982
+ return {
983
+ ...appendResult,
984
+ newMessages: outputMessages
985
+ };
986
+ });
987
+ return result;
988
+ },
989
+ fromWorkflowHandlerRetryOptions(
990
+ handleOptions && "retry" in handleOptions ? handleOptions.retry : options.retry
991
+ )
992
+ );
993
+ var withSession2 = (eventStore, callback) => {
994
+ const sessionFactory = canCreateEventStoreSession(eventStore) ? eventStore : nulloSessionFactory(eventStore);
995
+ return sessionFactory.withSession(callback);
996
+ };
997
+ var getWorkflowId = (options) => `emt:processor:workflow:${options.workflowName}`;
998
+ var workflowProcessor = (options) => {
999
+ const { workflow, ...rest } = options;
1000
+ const inputs = [...options.inputs.commands, ...options.inputs.events];
1001
+ let canHandle = inputs;
1002
+ if (options.separateInputInboxFromProcessing)
1003
+ canHandle = [
1004
+ ...canHandle,
1005
+ ...options.inputs.commands.map((t) => `${workflow.name}:${t}`),
1006
+ ...options.inputs.events.map((t) => `${workflow.name}:${t}`)
1007
+ ];
1008
+ if (options.outputHandler)
1009
+ canHandle = [...canHandle, ...options.outputHandler.canHandle];
1010
+ const handle = WorkflowHandler(options);
1011
+ return reactor({
1012
+ ...rest,
1013
+ processorId: options.processorId ?? getWorkflowId({ workflowName: workflow.name }),
1014
+ canHandle,
1015
+ type: MessageProcessorType.PROJECTOR,
1016
+ eachMessage: async (message2, context) => {
1017
+ const messageType = message2.type;
1018
+ const metadata = message2.metadata;
1019
+ const isInput = metadata?.input === true;
1020
+ if (isInput || inputs.includes(messageType)) {
1021
+ const result = await handle(
1022
+ context.connection.messageStore,
1023
+ message2,
1024
+ context
1025
+ );
1026
+ if (options.stopAfter && result.newMessages.length > 0) {
1027
+ for (const outputMessage of result.newMessages) {
1028
+ if (options.stopAfter(
1029
+ outputMessage
1030
+ )) {
1031
+ return { type: "STOP", reason: "Stop condition reached" };
1032
+ }
1033
+ }
1034
+ }
1035
+ return;
1036
+ }
1037
+ if (options.outputHandler?.canHandle.includes(messageType) === true) {
1038
+ const handledOutputMessages = await options.outputHandler.handle(
1039
+ message2,
1040
+ context
1041
+ );
1042
+ if (handledOutputMessages instanceof EmmettError) {
1043
+ return {
1044
+ type: "STOP",
1045
+ reason: "Routing error",
1046
+ error: handledOutputMessages
1047
+ };
1048
+ }
1049
+ const messagesToAppend = Array.isArray(handledOutputMessages) ? handledOutputMessages : handledOutputMessages ? [handledOutputMessages] : [];
1050
+ if (messagesToAppend.length === 0) {
1051
+ return;
1052
+ }
1053
+ const workflowId = options.getWorkflowId(
1054
+ message2
1055
+ );
1056
+ if (!workflowId) return;
1057
+ const streamName = options.mapWorkflowId ? options.mapWorkflowId(workflowId) : workflowStreamName({
1058
+ workflowName: workflow.name,
1059
+ workflowId
1060
+ });
1061
+ await context.connection.messageStore.appendToStream(
1062
+ streamName,
1063
+ messagesToAppend
1064
+ );
1065
+ return;
1066
+ }
1067
+ return;
1068
+ }
1069
+ });
1070
+ };
693
1071
 
694
1072
  // src/eventStore/schema/readLastMessageGlobalPosition.ts
695
1073
  import { singleOrNull, SQL } from "@event-driven-io/dumbo";
@@ -866,11 +1244,19 @@ var zipPostgreSQLEventStoreMessageBatchPullerStartFrom = (options) => {
866
1244
 
867
1245
  // src/eventStore/consumers/postgreSQLEventStoreConsumer.ts
868
1246
  import { dumbo as dumbo6 } from "@event-driven-io/dumbo";
869
- import { v7 as uuid7 } from "uuid";
1247
+ import { v7 as uuid9 } from "uuid";
870
1248
 
871
1249
  // src/eventStore/consumers/postgreSQLProcessor.ts
872
1250
  import { dumbo as dumbo5 } from "@event-driven-io/dumbo";
873
1251
 
1252
+ // src/eventStore/postgreSQLEventStore.ts
1253
+ import {
1254
+ dumbo as dumbo4,
1255
+ fromDatabaseDriverType,
1256
+ getFormatter,
1257
+ SQL as SQL20
1258
+ } from "@event-driven-io/dumbo";
1259
+
874
1260
  // src/eventStore/projections/locks/tryAcquireProjectionLock.ts
875
1261
  import { single } from "@event-driven-io/dumbo";
876
1262
 
@@ -1208,7 +1594,7 @@ var toProcessorLockKey = ({
1208
1594
 
1209
1595
  // src/eventStore/projections/management/projectionManagement.ts
1210
1596
  import {
1211
- JSONSerializer,
1597
+ JSONSerializer as JSONSerializer2,
1212
1598
  single as single3,
1213
1599
  singleOrNull as singleOrNull2,
1214
1600
  SQL as SQL7
@@ -1339,7 +1725,7 @@ var registerProjection = async (execute, options) => {
1339
1725
  const name = registration.projection.name;
1340
1726
  const version = registration.projection.version ?? 1;
1341
1727
  const kind = registration.projection.kind ?? registration.type;
1342
- const definition = JSONSerializer.serialize(registration.projection);
1728
+ const definition = JSONSerializer2.serialize(registration.projection);
1343
1729
  const lockKey = toProjectionLockKey({
1344
1730
  projectionName: name,
1345
1731
  partition,
@@ -1650,7 +2036,7 @@ var documentDoesNotExist = (options) => (assertOptions) => withCollection(
1650
2036
  const result = await collection.findOne(
1651
2037
  "withId" in options ? { _id: options.withId } : options.matchingFilter
1652
2038
  );
1653
- assertIsNotNull(result);
2039
+ assertIsNull(result);
1654
2040
  },
1655
2041
  { ...options, ...assertOptions }
1656
2042
  );
@@ -1699,140 +2085,382 @@ var expectPongoDocuments = {
1699
2085
 
1700
2086
  // src/eventStore/projections/postgresProjectionSpec.ts
1701
2087
  import {
1702
- dumbo as dumbo4
1703
- } from "@event-driven-io/dumbo";
1704
- import { v4 as uuid6 } from "uuid";
1705
-
1706
- // src/eventStore/postgreSQLEventStore.ts
1707
- import {
1708
- dumbo as dumbo3,
1709
- fromDatabaseDriverType,
1710
- getFormatter,
1711
- SQL as SQL20
1712
- } from "@event-driven-io/dumbo";
1713
-
1714
- // src/eventStore/schema/index.ts
1715
- import {
1716
- dumbo as dumbo2,
1717
- runSQLMigrations,
1718
- sqlMigration as sqlMigration4
1719
- } from "@event-driven-io/dumbo";
1720
-
1721
- // src/eventStore/schema/appendToStream.ts
1722
- import {
1723
- DumboError,
1724
- single as single4,
1725
- SQL as SQL8,
1726
- UniqueConstraintError
2088
+ dumbo
1727
2089
  } from "@event-driven-io/dumbo";
1728
- import { v4 as uuid5 } from "uuid";
1729
- var appendToStreamSQL = createFunctionIfDoesNotExistSQL(
1730
- "emt_append_to_stream",
1731
- SQL8`CREATE OR REPLACE FUNCTION emt_append_to_stream(
1732
- v_message_ids text[],
1733
- v_messages_data jsonb[],
1734
- v_messages_metadata jsonb[],
1735
- v_message_schema_versions text[],
1736
- v_message_types text[],
1737
- v_message_kinds text[],
1738
- v_stream_id text,
1739
- v_stream_type text,
1740
- v_expected_stream_position bigint DEFAULT NULL,
1741
- v_partition text DEFAULT emt_sanitize_name('default_partition')
1742
- ) RETURNS TABLE (
1743
- success boolean,
1744
- next_stream_position bigint,
1745
- global_positions bigint[],
1746
- transaction_id xid8
1747
- ) LANGUAGE plpgsql
1748
- AS $emt_append_to_stream$
1749
- DECLARE
1750
- v_next_stream_position bigint;
1751
- v_position bigint;
1752
- v_updated_rows int;
1753
- v_transaction_id xid8;
1754
- v_global_positions bigint[];
1755
- BEGIN
1756
- v_transaction_id := pg_current_xact_id();
1757
-
1758
- IF v_expected_stream_position IS NULL THEN
1759
- SELECT COALESCE(
1760
- (SELECT stream_position
1761
- FROM ${SQL8.identifier(streamsTable.name)}
1762
- WHERE stream_id = v_stream_id
1763
- AND partition = v_partition
1764
- AND is_archived = FALSE
1765
- LIMIT 1),
1766
- 0
1767
- ) INTO v_expected_stream_position;
1768
- END IF;
1769
-
1770
- v_next_stream_position := v_expected_stream_position + array_upper(v_messages_data, 1);
1771
-
1772
- IF v_expected_stream_position = 0 THEN
1773
- INSERT INTO ${SQL8.identifier(streamsTable.name)}
1774
- (stream_id, stream_position, partition, stream_type, stream_metadata, is_archived)
1775
- VALUES
1776
- (v_stream_id, v_next_stream_position, v_partition, v_stream_type, '{}', FALSE);
1777
- ELSE
1778
- UPDATE ${SQL8.identifier(streamsTable.name)} as s
1779
- SET stream_position = v_next_stream_position
1780
- WHERE stream_id = v_stream_id AND stream_position = v_expected_stream_position AND partition = v_partition AND is_archived = FALSE;
1781
-
1782
- get diagnostics v_updated_rows = row_count;
1783
-
1784
- IF v_updated_rows = 0 THEN
1785
- RETURN QUERY SELECT FALSE, NULL::bigint, NULL::bigint[], NULL::xid8;
1786
- RETURN;
1787
- END IF;
1788
- END IF;
1789
-
1790
- WITH ev AS (
1791
- SELECT row_number() OVER () + v_expected_stream_position AS stream_position,
1792
- message_data,
1793
- message_metadata,
1794
- schema_version,
1795
- message_id,
1796
- message_type,
1797
- message_kind
1798
- FROM (
1799
- SELECT *
1800
- FROM
1801
- unnest(v_message_ids, v_messages_data, v_messages_metadata, v_message_schema_versions, v_message_types, v_message_kinds)
1802
- AS message(message_id, message_data, message_metadata, schema_version, message_type, message_kind)
1803
- ) AS message
1804
- ),
1805
- all_messages_insert AS (
1806
- INSERT INTO ${SQL8.identifier(messagesTable.name)}
1807
- (stream_id, stream_position, partition, message_data, message_metadata, message_schema_version, message_type, message_kind, message_id, transaction_id)
1808
- SELECT
1809
- v_stream_id, ev.stream_position, v_partition, ev.message_data, ev.message_metadata, ev.schema_version, ev.message_type, ev.message_kind, ev.message_id, v_transaction_id
1810
- FROM ev
1811
- RETURNING global_position
1812
- )
1813
- SELECT
1814
- array_agg(global_position ORDER BY global_position) INTO v_global_positions
1815
- FROM
1816
- all_messages_insert;
1817
-
1818
- RETURN QUERY SELECT TRUE, v_next_stream_position, v_global_positions, v_transaction_id;
1819
- END;
1820
- $emt_append_to_stream$;
1821
- `
1822
- );
1823
- var callAppendToStream = (params) => SQL8`SELECT * FROM emt_append_to_stream(
1824
- ${params.messageIds},
1825
- ${params.messagesData},
1826
- ${params.messagesMetadata},
1827
- ${params.schemaVersions},
1828
- ${params.messageTypes},
1829
- ${params.messageKinds},
1830
- ${params.streamId}::text,
1831
- ${params.streamType}::text,
1832
- ${params.expectedStreamPosition},
1833
- ${params.partition}::text
1834
- )`;
1835
- var appendToStream = (pool, streamName, streamType, messages, options) => pool.withTransaction(async (transaction) => {
2090
+ import { v4 as uuid7 } from "uuid";
2091
+ var PostgreSQLProjectionSpec = {
2092
+ for: (options) => {
2093
+ {
2094
+ const { projection: projection2, ...restOptions } = options;
2095
+ const dumboOptions = {
2096
+ ...restOptions,
2097
+ serialization: projection2.serialization
2098
+ };
2099
+ const { connectionString } = dumboOptions;
2100
+ let wasInitialised = false;
2101
+ const initialize = async (pool) => {
2102
+ const eventStore = getPostgreSQLEventStore(connectionString, {
2103
+ // TODO: This will need to change when we support other drivers
2104
+ connectionOptions: { dumbo: pool }
2105
+ });
2106
+ if (wasInitialised) return;
2107
+ wasInitialised = true;
2108
+ await eventStore.schema.migrate();
2109
+ if (projection2.init)
2110
+ await pool.withTransaction(async (transaction) => {
2111
+ await projection2.init({
2112
+ registrationType: "async",
2113
+ version: projection2.version ?? 1,
2114
+ status: "active",
2115
+ context: await transactionToPostgreSQLProjectionHandlerContext(
2116
+ connectionString,
2117
+ pool,
2118
+ transaction
2119
+ )
2120
+ });
2121
+ });
2122
+ };
2123
+ return (givenEvents) => {
2124
+ return {
2125
+ when: (events, options2) => {
2126
+ const allEvents = [];
2127
+ const run = async (pool) => {
2128
+ let globalPosition = 0n;
2129
+ const numberOfTimes = options2?.numberOfTimes ?? 1;
2130
+ for (const event of [
2131
+ ...givenEvents,
2132
+ ...Array.from({ length: numberOfTimes }).flatMap(() => events)
2133
+ ]) {
2134
+ const metadata = {
2135
+ checkpoint: bigIntProcessorCheckpoint(++globalPosition),
2136
+ globalPosition,
2137
+ streamPosition: globalPosition,
2138
+ streamName: `test-${uuid7()}`,
2139
+ messageId: uuid7()
2140
+ };
2141
+ allEvents.push({
2142
+ ...event,
2143
+ kind: "Event",
2144
+ metadata: {
2145
+ ...metadata,
2146
+ ..."metadata" in event ? event.metadata ?? {} : {}
2147
+ }
2148
+ });
2149
+ }
2150
+ await initialize(pool);
2151
+ await pool.withTransaction(async (transaction) => {
2152
+ await handleProjections({
2153
+ events: allEvents,
2154
+ projections: [projection2],
2155
+ ...await transactionToPostgreSQLProjectionHandlerContext(
2156
+ connectionString,
2157
+ pool,
2158
+ transaction
2159
+ )
2160
+ });
2161
+ });
2162
+ };
2163
+ return {
2164
+ then: async (assert, message) => {
2165
+ const pool = dumbo(dumboOptions);
2166
+ try {
2167
+ await run(pool);
2168
+ const succeeded = await assert({ pool, connectionString });
2169
+ if (succeeded !== void 0 && succeeded === false)
2170
+ assertFails(
2171
+ message ?? "Projection specification didn't match the criteria"
2172
+ );
2173
+ } finally {
2174
+ await pool.close();
2175
+ }
2176
+ },
2177
+ thenThrows: async (...args) => {
2178
+ const pool = dumbo(dumboOptions);
2179
+ try {
2180
+ await run(pool);
2181
+ throw new AssertionError("Handler did not fail as expected");
2182
+ } catch (error) {
2183
+ if (error instanceof AssertionError) throw error;
2184
+ if (args.length === 0) return;
2185
+ if (!isErrorConstructor(args[0])) {
2186
+ assertTrue(
2187
+ args[0](error),
2188
+ `Error didn't match the error condition: ${error?.toString()}`
2189
+ );
2190
+ return;
2191
+ }
2192
+ assertTrue(
2193
+ error instanceof args[0],
2194
+ `Caught error is not an instance of the expected type: ${error?.toString()}`
2195
+ );
2196
+ if (args[1]) {
2197
+ assertTrue(
2198
+ args[1](error),
2199
+ `Error didn't match the error condition: ${error?.toString()}`
2200
+ );
2201
+ }
2202
+ } finally {
2203
+ await pool.close();
2204
+ }
2205
+ }
2206
+ };
2207
+ }
2208
+ };
2209
+ };
2210
+ }
2211
+ }
2212
+ };
2213
+ var eventInStream = (streamName, event) => {
2214
+ return {
2215
+ ...event,
2216
+ metadata: {
2217
+ ...event.metadata ?? {},
2218
+ streamName: event.metadata?.streamName ?? streamName
2219
+ }
2220
+ };
2221
+ };
2222
+ var eventsInStream = (streamName, events) => {
2223
+ return events.map((e) => eventInStream(streamName, e));
2224
+ };
2225
+ var newEventsInStream = eventsInStream;
2226
+ var assertSQLQueryResultMatches = (sql, rows) => async ({ pool: { execute } }) => {
2227
+ const result = await execute.query(sql);
2228
+ assertThatArray(rows).containsExactlyInAnyOrder(result.rows);
2229
+ };
2230
+ var expectSQL = {
2231
+ query: (sql) => ({
2232
+ resultRows: {
2233
+ toBeTheSame: (rows) => assertSQLQueryResultMatches(sql, rows)
2234
+ }
2235
+ })
2236
+ };
2237
+
2238
+ // src/eventStore/projections/postgreSQLProjection.ts
2239
+ var transactionToPostgreSQLProjectionHandlerContext = async (connectionString, pool, transaction) => ({
2240
+ execute: transaction.execute,
2241
+ connection: {
2242
+ connectionString,
2243
+ client: await transaction.connection.open(),
2244
+ transaction,
2245
+ pool
2246
+ }
2247
+ });
2248
+ var handleProjections = async (options) => {
2249
+ const {
2250
+ projections: allProjections,
2251
+ events,
2252
+ connection: { pool, transaction, connectionString },
2253
+ partition = defaultTag2
2254
+ } = options;
2255
+ const eventTypes = events.map((e) => e.type);
2256
+ const projections = allProjections.filter(
2257
+ (p) => p.canHandle.some((type) => eventTypes.includes(type))
2258
+ );
2259
+ const client = await transaction.connection.open();
2260
+ for (const projection2 of projections) {
2261
+ if (projection2.name) {
2262
+ const lockAcquired = await postgreSQLProjectionLock({
2263
+ projectionName: projection2.name,
2264
+ partition,
2265
+ version: projection2.version ?? 1
2266
+ }).tryAcquire({ execute: transaction.execute });
2267
+ if (!lockAcquired) {
2268
+ continue;
2269
+ }
2270
+ }
2271
+ await projection2.handle(events, {
2272
+ connection: {
2273
+ connectionString,
2274
+ pool,
2275
+ client,
2276
+ transaction
2277
+ },
2278
+ execute: transaction.execute
2279
+ });
2280
+ }
2281
+ };
2282
+ var postgreSQLProjection = (definition) => projection({
2283
+ ...definition,
2284
+ init: async (options) => {
2285
+ await registerProjection(options.context.execute, {
2286
+ // TODO: pass partition from options
2287
+ partition: defaultTag2,
2288
+ status: "active",
2289
+ registration: {
2290
+ type: "async",
2291
+ // TODO: fix this
2292
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
2293
+ projection: definition
2294
+ }
2295
+ });
2296
+ if (definition.init) {
2297
+ await definition.init(options);
2298
+ }
2299
+ }
2300
+ });
2301
+ var postgreSQLRawBatchSQLProjection = (options) => postgreSQLProjection({
2302
+ name: options.name,
2303
+ kind: options.kind ?? "emt:projections:postgresql:raw_sql:batch",
2304
+ version: options.version,
2305
+ canHandle: options.canHandle,
2306
+ eventsOptions: options.eventsOptions,
2307
+ handle: async (events, context) => {
2308
+ const sqls = await options.evolve(events, context);
2309
+ await context.execute.batchCommand(sqls);
2310
+ },
2311
+ init: async (initOptions) => {
2312
+ const initSQL = options.init ? await options.init(initOptions) : void 0;
2313
+ if (initSQL) {
2314
+ if (Array.isArray(initSQL)) {
2315
+ await initOptions.context.execute.batchCommand(initSQL);
2316
+ } else {
2317
+ await initOptions.context.execute.command(initSQL);
2318
+ }
2319
+ }
2320
+ }
2321
+ });
2322
+ var postgreSQLRawSQLProjection = (options) => {
2323
+ const { evolve, kind, ...rest } = options;
2324
+ return postgreSQLRawBatchSQLProjection({
2325
+ kind: kind ?? "emt:projections:postgresql:raw:_sql:single",
2326
+ ...rest,
2327
+ evolve: async (events, context) => {
2328
+ const sqls = [];
2329
+ for (const event of events) {
2330
+ const pendingSqls = await evolve(event, context);
2331
+ if (Array.isArray(pendingSqls)) {
2332
+ sqls.push(...pendingSqls);
2333
+ } else {
2334
+ sqls.push(pendingSqls);
2335
+ }
2336
+ }
2337
+ return sqls;
2338
+ }
2339
+ });
2340
+ };
2341
+
2342
+ // src/eventStore/schema/index.ts
2343
+ import {
2344
+ dumbo as dumbo3,
2345
+ runSQLMigrations,
2346
+ sqlMigration as sqlMigration4
2347
+ } from "@event-driven-io/dumbo";
2348
+
2349
+ // src/eventStore/schema/appendToStream.ts
2350
+ import {
2351
+ DumboError,
2352
+ single as single4,
2353
+ SQL as SQL8,
2354
+ UniqueConstraintError
2355
+ } from "@event-driven-io/dumbo";
2356
+ import { v4 as uuid8 } from "uuid";
2357
+ var appendToStreamSQL = createFunctionIfDoesNotExistSQL(
2358
+ "emt_append_to_stream",
2359
+ SQL8`CREATE OR REPLACE FUNCTION emt_append_to_stream(
2360
+ v_message_ids text[],
2361
+ v_messages_data jsonb[],
2362
+ v_messages_metadata jsonb[],
2363
+ v_message_schema_versions text[],
2364
+ v_message_types text[],
2365
+ v_message_kinds text[],
2366
+ v_stream_id text,
2367
+ v_stream_type text,
2368
+ v_expected_stream_position bigint DEFAULT NULL,
2369
+ v_partition text DEFAULT emt_sanitize_name('default_partition')
2370
+ ) RETURNS TABLE (
2371
+ success boolean,
2372
+ next_stream_position bigint,
2373
+ global_positions bigint[],
2374
+ transaction_id xid8
2375
+ ) LANGUAGE plpgsql
2376
+ AS $emt_append_to_stream$
2377
+ DECLARE
2378
+ v_next_stream_position bigint;
2379
+ v_position bigint;
2380
+ v_updated_rows int;
2381
+ v_transaction_id xid8;
2382
+ v_global_positions bigint[];
2383
+ BEGIN
2384
+ v_transaction_id := pg_current_xact_id();
2385
+
2386
+ IF v_expected_stream_position IS NULL THEN
2387
+ SELECT COALESCE(
2388
+ (SELECT stream_position
2389
+ FROM ${SQL8.identifier(streamsTable.name)}
2390
+ WHERE stream_id = v_stream_id
2391
+ AND partition = v_partition
2392
+ AND is_archived = FALSE
2393
+ LIMIT 1),
2394
+ 0
2395
+ ) INTO v_expected_stream_position;
2396
+ END IF;
2397
+
2398
+ v_next_stream_position := v_expected_stream_position + array_upper(v_messages_data, 1);
2399
+
2400
+ IF v_expected_stream_position = 0 THEN
2401
+ INSERT INTO ${SQL8.identifier(streamsTable.name)}
2402
+ (stream_id, stream_position, partition, stream_type, stream_metadata, is_archived)
2403
+ VALUES
2404
+ (v_stream_id, v_next_stream_position, v_partition, v_stream_type, '{}', FALSE);
2405
+ ELSE
2406
+ UPDATE ${SQL8.identifier(streamsTable.name)} as s
2407
+ SET stream_position = v_next_stream_position
2408
+ WHERE stream_id = v_stream_id AND stream_position = v_expected_stream_position AND partition = v_partition AND is_archived = FALSE;
2409
+
2410
+ get diagnostics v_updated_rows = row_count;
2411
+
2412
+ IF v_updated_rows = 0 THEN
2413
+ RETURN QUERY SELECT FALSE, NULL::bigint, NULL::bigint[], NULL::xid8;
2414
+ RETURN;
2415
+ END IF;
2416
+ END IF;
2417
+
2418
+ WITH ev AS (
2419
+ SELECT row_number() OVER () + v_expected_stream_position AS stream_position,
2420
+ message_data,
2421
+ message_metadata,
2422
+ schema_version,
2423
+ message_id,
2424
+ message_type,
2425
+ message_kind
2426
+ FROM (
2427
+ SELECT *
2428
+ FROM
2429
+ unnest(v_message_ids, v_messages_data, v_messages_metadata, v_message_schema_versions, v_message_types, v_message_kinds)
2430
+ AS message(message_id, message_data, message_metadata, schema_version, message_type, message_kind)
2431
+ ) AS message
2432
+ ),
2433
+ all_messages_insert AS (
2434
+ INSERT INTO ${SQL8.identifier(messagesTable.name)}
2435
+ (stream_id, stream_position, partition, message_data, message_metadata, message_schema_version, message_type, message_kind, message_id, transaction_id)
2436
+ SELECT
2437
+ v_stream_id, ev.stream_position, v_partition, ev.message_data, ev.message_metadata, ev.schema_version, ev.message_type, ev.message_kind, ev.message_id, v_transaction_id
2438
+ FROM ev
2439
+ RETURNING global_position
2440
+ )
2441
+ SELECT
2442
+ array_agg(global_position ORDER BY global_position) INTO v_global_positions
2443
+ FROM
2444
+ all_messages_insert;
2445
+
2446
+ RETURN QUERY SELECT TRUE, v_next_stream_position, v_global_positions, v_transaction_id;
2447
+ END;
2448
+ $emt_append_to_stream$;
2449
+ `
2450
+ );
2451
+ var callAppendToStream = (params) => SQL8`SELECT * FROM emt_append_to_stream(
2452
+ ${params.messageIds},
2453
+ ${params.messagesData},
2454
+ ${params.messagesMetadata},
2455
+ ${params.schemaVersions},
2456
+ ${params.messageTypes},
2457
+ ${params.messageKinds},
2458
+ ${params.streamId}::text,
2459
+ ${params.streamType}::text,
2460
+ ${params.expectedStreamPosition},
2461
+ ${params.partition}::text
2462
+ )`;
2463
+ var appendToStream = (pool, streamName, streamType, messages, options) => pool.withTransaction(async (transaction) => {
1836
2464
  const { execute } = transaction;
1837
2465
  if (messages.length === 0)
1838
2466
  return { success: false, result: { success: false } };
@@ -1844,7 +2472,7 @@ var appendToStream = (pool, streamName, streamType, messages, options) => pool.w
1844
2472
  ...e,
1845
2473
  kind: e.kind ?? "Event",
1846
2474
  metadata: {
1847
- messageId: uuid5(),
2475
+ messageId: uuid8(),
1848
2476
  ..."metadata" in e ? e.metadata ?? {} : {}
1849
2477
  }
1850
2478
  }));
@@ -3388,1175 +4016,930 @@ BEGIN
3388
4016
  WITH lock_check AS (
3389
4017
  SELECT pg_try_advisory_xact_lock(p_lock_key) AS lock_acquired
3390
4018
  ),
3391
- update_result AS (
3392
- UPDATE emt_projections
3393
- SET status = 'active',
3394
- last_updated = now()
3395
- WHERE name = p_name
3396
- AND partition = p_partition
3397
- AND version = p_version
3398
- AND (SELECT lock_acquired FROM lock_check) = true
3399
- RETURNING name
3400
- )
3401
- SELECT COUNT(*) > 0 INTO v_result FROM update_result;
3402
-
3403
- RETURN v_result;
3404
- END;
3405
- $emt_activate_projection$;
3406
-
3407
- CREATE OR REPLACE FUNCTION emt_deactivate_projection(
3408
- p_lock_key BIGINT,
3409
- p_name TEXT,
3410
- p_partition TEXT,
3411
- p_version INT
3412
- )
3413
- RETURNS BOOLEAN
3414
- LANGUAGE plpgsql
3415
- AS $emt_deactivate_projection$
3416
- DECLARE
3417
- v_result BOOLEAN;
3418
- BEGIN
3419
- WITH lock_check AS (
3420
- SELECT pg_try_advisory_xact_lock(p_lock_key) AS lock_acquired
3421
- ),
3422
- update_result AS (
3423
- UPDATE emt_projections
3424
- SET status = 'inactive',
3425
- last_updated = now()
3426
- WHERE name = p_name
3427
- AND partition = p_partition
3428
- AND version = p_version
3429
- AND (SELECT lock_acquired FROM lock_check) = true
3430
- RETURNING name
3431
- )
3432
- SELECT COUNT(*) > 0 INTO v_result FROM update_result;
3433
-
3434
- RETURN v_result;
3435
- END;
3436
- $emt_deactivate_projection$;
3437
- `;
3438
-
3439
- // src/eventStore/schema/storeProcessorCheckpoint.ts
3440
- import { single as single5, SQL as SQL13 } from "@event-driven-io/dumbo";
3441
- var storeSubscriptionCheckpointSQL = createFunctionIfDoesNotExistSQL(
3442
- "store_processor_checkpoint",
3443
- SQL13`
3444
- CREATE OR REPLACE FUNCTION store_processor_checkpoint(
3445
- p_processor_id TEXT,
3446
- p_version BIGINT,
3447
- p_position TEXT,
3448
- p_check_position TEXT,
3449
- p_transaction_id xid8,
3450
- p_partition TEXT DEFAULT '${SQL13.plain(defaultTag2)}',
3451
- p_processor_instance_id TEXT DEFAULT '${SQL13.plain(unknownTag2)}'
3452
- ) RETURNS INT AS $spc$
3453
- DECLARE
3454
- current_position TEXT;
3455
- BEGIN
3456
- -- Handle the case when p_check_position is provided
3457
- IF p_check_position IS NOT NULL THEN
3458
- -- Try to update if the position matches p_check_position
3459
- UPDATE "${SQL13.plain(processorsTable.name)}"
3460
- SET
3461
- "last_processed_checkpoint" = p_position,
3462
- "last_processed_transaction_id" = p_transaction_id,
3463
- "last_updated" = now()
3464
- WHERE "processor_id" = p_processor_id
3465
- AND "last_processed_checkpoint" = p_check_position
3466
- AND "partition" = p_partition
3467
- AND "version" = p_version;
3468
-
3469
- IF FOUND THEN
3470
- RETURN 1; -- Successfully updated
3471
- END IF;
3472
-
3473
- -- Retrieve the current position
3474
- SELECT "last_processed_checkpoint" INTO current_position
3475
- FROM "${SQL13.plain(processorsTable.name)}"
3476
- WHERE "processor_id" = p_processor_id
3477
- AND "partition" = p_partition
3478
- AND "version" = p_version;
3479
-
3480
- -- Return appropriate codes based on current position
3481
- IF current_position = p_position THEN
3482
- RETURN 0; -- Idempotent check: position already set
3483
- ELSIF current_position > p_position THEN
3484
- RETURN 3; -- Current ahead: another process has progressed further
3485
- ELSE
3486
- RETURN 2; -- Mismatch: check position doesn't match current
3487
- END IF;
3488
- END IF;
3489
-
3490
- -- Handle the case when p_check_position is NULL: Insert if not exists
3491
- BEGIN
3492
- INSERT INTO "${SQL13.plain(processorsTable.name)}"("processor_id", "version", "last_processed_checkpoint", "partition", "last_processed_transaction_id", "created_at", "last_updated")
3493
- VALUES (p_processor_id, p_version, p_position, p_partition, p_transaction_id, now(), now());
3494
- RETURN 1; -- Successfully inserted
3495
- EXCEPTION WHEN unique_violation THEN
3496
- -- If insertion failed, it means the row already exists
3497
- SELECT "last_processed_checkpoint" INTO current_position
3498
- FROM "${SQL13.plain(processorsTable.name)}"
3499
- WHERE "processor_id" = p_processor_id
3500
- AND "partition" = p_partition
3501
- AND "version" = p_version;
3502
-
3503
- IF current_position = p_position THEN
3504
- RETURN 0; -- Idempotent check: position already set
3505
- ELSIF current_position > p_position THEN
3506
- RETURN 3; -- Current ahead: another process has progressed further
3507
- ELSE
3508
- RETURN 2; -- Insertion failed, row already exists with different position
3509
- END IF;
3510
- END;
3511
- END;
3512
- $spc$ LANGUAGE plpgsql;
3513
- `
3514
- );
3515
- var callStoreProcessorCheckpoint = (params) => SQL13`
3516
- SELECT store_processor_checkpoint(
3517
- ${params.processorId},
3518
- ${params.version},
3519
- ${params.position},
3520
- ${params.checkPosition},
3521
- pg_current_xact_id(),
3522
- ${params.partition},
3523
- ${params.processorInstanceId}
3524
- ) as result;`;
3525
- var storeProcessorCheckpoint = async (execute, options) => {
3526
- try {
3527
- const { result } = await single5(
3528
- execute.command(
3529
- callStoreProcessorCheckpoint({
3530
- processorId: options.processorId,
3531
- version: options.version ?? 1,
3532
- position: options.newCheckpoint !== null ? options.newCheckpoint : null,
3533
- checkPosition: options.lastProcessedCheckpoint !== null ? options.lastProcessedCheckpoint : null,
3534
- partition: options.partition ?? defaultTag2,
3535
- processorInstanceId: options.processorInstanceId ?? unknownTag2
3536
- })
3537
- )
3538
- );
3539
- return result === 1 ? { success: true, newCheckpoint: options.newCheckpoint } : {
3540
- success: false,
3541
- reason: result === 0 ? "IGNORED" : result === 3 ? "CURRENT_AHEAD" : "MISMATCH"
3542
- };
3543
- } catch (error) {
3544
- console.log(error);
3545
- throw error;
3546
- }
3547
- };
3548
-
3549
- // src/eventStore/schema/tables.ts
3550
- import { SQL as SQL15 } from "@event-driven-io/dumbo";
3551
-
3552
- // src/eventStore/schema/migrations/0_43_0/index.ts
3553
- import {
3554
- dumbo,
3555
- SQL as SQL14,
3556
- sqlMigration as sqlMigration3
3557
- } from "@event-driven-io/dumbo";
3558
- var migration_0_43_0_cleanupLegacySubscriptionSQL = SQL14`
3559
- DO $$
3560
- BEGIN
3561
- IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'emt_subscriptions') THEN
3562
- -- Restore clean emt_add_partition (remove creation of emt_subscriptions partitions)
3563
- CREATE OR REPLACE FUNCTION emt_add_partition(partition_name TEXT) RETURNS void AS $fnpar$
3564
- BEGIN
3565
- PERFORM emt_add_table_partition('${SQL14.plain(messagesTable.name)}', partition_name);
3566
- PERFORM emt_add_table_partition('${SQL14.plain(streamsTable.name)}', partition_name);
3567
-
3568
- EXECUTE format('
3569
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3570
- FOR VALUES IN (%L);',
3571
- emt_sanitize_name('${SQL14.plain(processorsTable.name)}' || '_' || partition_name), '${SQL14.plain(processorsTable.name)}', partition_name
3572
- );
3573
-
3574
- EXECUTE format('
3575
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3576
- FOR VALUES IN (%L);',
3577
- emt_sanitize_name('${SQL14.plain(projectionsTable.name)}' || '_' || partition_name), '${SQL14.plain(projectionsTable.name)}', partition_name
3578
- );
3579
- END;
3580
- $fnpar$ LANGUAGE plpgsql;
3581
-
3582
- -- Drop old subscriptions table if it exists
3583
- DROP TABLE IF EXISTS emt_subscriptions CASCADE;
3584
-
3585
- -- Drop old function if it exists
3586
- DROP FUNCTION IF EXISTS store_subscription_checkpoint(character varying, bigint, bigint, bigint, xid8, text);
3587
-
3588
- -- Restore clean store_processor_checkpoint (remove dual-write logic)
3589
- CREATE OR REPLACE FUNCTION store_processor_checkpoint(
3590
- p_processor_id TEXT,
3591
- p_version BIGINT,
3592
- p_position TEXT,
3593
- p_check_position TEXT,
3594
- p_transaction_id xid8,
3595
- p_partition TEXT DEFAULT '${SQL14.plain(defaultTag2)}',
3596
- p_processor_instance_id TEXT DEFAULT 'emt:unknown'
3597
- ) RETURNS INT AS $fn$
3598
- DECLARE
3599
- current_position TEXT;
3600
- BEGIN
3601
- IF p_check_position IS NOT NULL THEN
3602
- UPDATE "emt_processors"
3603
- SET
3604
- "last_processed_checkpoint" = p_position,
3605
- "last_processed_transaction_id" = p_transaction_id,
3606
- "last_updated" = now()
3607
- WHERE "processor_id" = p_processor_id
3608
- AND "last_processed_checkpoint" = p_check_position
3609
- AND "partition" = p_partition
3610
- AND "version" = p_version;
4019
+ update_result AS (
4020
+ UPDATE emt_projections
4021
+ SET status = 'active',
4022
+ last_updated = now()
4023
+ WHERE name = p_name
4024
+ AND partition = p_partition
4025
+ AND version = p_version
4026
+ AND (SELECT lock_acquired FROM lock_check) = true
4027
+ RETURNING name
4028
+ )
4029
+ SELECT COUNT(*) > 0 INTO v_result FROM update_result;
3611
4030
 
3612
- IF FOUND THEN
3613
- RETURN 1;
3614
- END IF;
4031
+ RETURN v_result;
4032
+ END;
4033
+ $emt_activate_projection$;
3615
4034
 
3616
- SELECT "last_processed_checkpoint" INTO current_position
3617
- FROM "emt_processors"
3618
- WHERE "processor_id" = p_processor_id
3619
- AND "partition" = p_partition
3620
- AND "version" = p_version ;
4035
+ CREATE OR REPLACE FUNCTION emt_deactivate_projection(
4036
+ p_lock_key BIGINT,
4037
+ p_name TEXT,
4038
+ p_partition TEXT,
4039
+ p_version INT
4040
+ )
4041
+ RETURNS BOOLEAN
4042
+ LANGUAGE plpgsql
4043
+ AS $emt_deactivate_projection$
4044
+ DECLARE
4045
+ v_result BOOLEAN;
4046
+ BEGIN
4047
+ WITH lock_check AS (
4048
+ SELECT pg_try_advisory_xact_lock(p_lock_key) AS lock_acquired
4049
+ ),
4050
+ update_result AS (
4051
+ UPDATE emt_projections
4052
+ SET status = 'inactive',
4053
+ last_updated = now()
4054
+ WHERE name = p_name
4055
+ AND partition = p_partition
4056
+ AND version = p_version
4057
+ AND (SELECT lock_acquired FROM lock_check) = true
4058
+ RETURNING name
4059
+ )
4060
+ SELECT COUNT(*) > 0 INTO v_result FROM update_result;
3621
4061
 
3622
- IF current_position = p_position THEN
3623
- RETURN 0;
3624
- ELSIF current_position > p_position THEN
3625
- RETURN 3;
3626
- ELSE
3627
- RETURN 2;
3628
- END IF;
3629
- END IF;
4062
+ RETURN v_result;
4063
+ END;
4064
+ $emt_deactivate_projection$;
4065
+ `;
3630
4066
 
3631
- BEGIN
3632
- INSERT INTO "emt_processors"("processor_id", "version", "last_processed_checkpoint", "partition", "last_processed_transaction_id", "created_at", "last_updated")
3633
- VALUES (p_processor_id, p_version, p_position, p_partition, p_transaction_id, now(), now());
3634
- RETURN 1;
3635
- EXCEPTION WHEN unique_violation THEN
3636
- SELECT "last_processed_checkpoint" INTO current_position
3637
- FROM "emt_processors"
3638
- WHERE "processor_id" = p_processor_id
3639
- AND "partition" = p_partition
3640
- AND "version" = p_version;
4067
+ // src/eventStore/schema/storeProcessorCheckpoint.ts
4068
+ import { single as single5, SQL as SQL13 } from "@event-driven-io/dumbo";
4069
+ var storeSubscriptionCheckpointSQL = createFunctionIfDoesNotExistSQL(
4070
+ "store_processor_checkpoint",
4071
+ SQL13`
4072
+ CREATE OR REPLACE FUNCTION store_processor_checkpoint(
4073
+ p_processor_id TEXT,
4074
+ p_version BIGINT,
4075
+ p_position TEXT,
4076
+ p_check_position TEXT,
4077
+ p_transaction_id xid8,
4078
+ p_partition TEXT DEFAULT '${SQL13.plain(defaultTag2)}',
4079
+ p_processor_instance_id TEXT DEFAULT '${SQL13.plain(unknownTag2)}'
4080
+ ) RETURNS INT AS $spc$
4081
+ DECLARE
4082
+ current_position TEXT;
4083
+ BEGIN
4084
+ -- Handle the case when p_check_position is provided
4085
+ IF p_check_position IS NOT NULL THEN
4086
+ -- Try to update if the position matches p_check_position
4087
+ UPDATE "${SQL13.plain(processorsTable.name)}"
4088
+ SET
4089
+ "last_processed_checkpoint" = p_position,
4090
+ "last_processed_transaction_id" = p_transaction_id,
4091
+ "last_updated" = now()
4092
+ WHERE "processor_id" = p_processor_id
4093
+ AND "last_processed_checkpoint" = p_check_position
4094
+ AND "partition" = p_partition
4095
+ AND "version" = p_version;
3641
4096
 
3642
- IF current_position = p_position THEN
3643
- RETURN 0;
3644
- ELSE
3645
- RETURN 2;
3646
- END IF;
3647
- END;
3648
- END;
3649
- $fn$ LANGUAGE plpgsql;
3650
- END IF;
3651
- END $$;
3652
- `;
3653
- var migration_0_43_0_cleanupLegacySubscription = sqlMigration3("emt:postgresql:eventstore:0.43.0:cleanup-legacy-subscription", [
3654
- migration_0_43_0_cleanupLegacySubscriptionSQL
3655
- ]);
3656
- var cleanupLegacySubscriptionTables = async (connectionString) => {
3657
- const pool = dumbo({ connectionString });
3658
- try {
3659
- await pool.withTransaction(async ({ execute }) => {
3660
- await execute.command(migration_0_43_0_cleanupLegacySubscriptionSQL);
3661
- });
3662
- } finally {
3663
- await pool.close();
3664
- }
3665
- };
4097
+ IF FOUND THEN
4098
+ RETURN 1; -- Successfully updated
4099
+ END IF;
3666
4100
 
3667
- // src/eventStore/schema/tables.ts
3668
- var streamsTableSQL = SQL15`
3669
- CREATE TABLE IF NOT EXISTS ${SQL15.identifier(streamsTable.name)}(
3670
- stream_id TEXT NOT NULL,
3671
- stream_position BIGINT NOT NULL,
3672
- partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
3673
- stream_type TEXT NOT NULL,
3674
- stream_metadata JSONB NOT NULL,
3675
- is_archived BOOLEAN NOT NULL DEFAULT FALSE,
3676
- PRIMARY KEY (stream_id, partition, is_archived)
3677
- ) PARTITION BY LIST (partition);
3678
-
3679
- CREATE UNIQUE INDEX IF NOT EXISTS idx_streams_unique
3680
- ON ${SQL15.identifier(streamsTable.name)}(stream_id, partition, is_archived)
3681
- INCLUDE (stream_position);`;
3682
- var messagesTableSQL = SQL15`
3683
- CREATE SEQUENCE IF NOT EXISTS emt_global_message_position;
4101
+ -- Retrieve the current position
4102
+ SELECT "last_processed_checkpoint" INTO current_position
4103
+ FROM "${SQL13.plain(processorsTable.name)}"
4104
+ WHERE "processor_id" = p_processor_id
4105
+ AND "partition" = p_partition
4106
+ AND "version" = p_version;
3684
4107
 
3685
- CREATE TABLE IF NOT EXISTS ${SQL15.identifier(messagesTable.name)}(
3686
- stream_position BIGINT NOT NULL,
3687
- global_position BIGINT DEFAULT nextval('emt_global_message_position'),
3688
- transaction_id XID8 NOT NULL,
3689
- created TIMESTAMPTZ NOT NULL DEFAULT now(),
3690
- is_archived BOOLEAN NOT NULL DEFAULT FALSE,
3691
- message_kind VARCHAR(1) NOT NULL DEFAULT 'E',
3692
- stream_id TEXT NOT NULL,
3693
- partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
3694
- message_schema_version TEXT NOT NULL,
3695
- message_id TEXT NOT NULL,
3696
- message_type TEXT NOT NULL,
3697
- message_data JSONB NOT NULL,
3698
- message_metadata JSONB NOT NULL,
3699
- PRIMARY KEY (stream_id, stream_position, partition, is_archived)
3700
- ) PARTITION BY LIST (partition);`;
3701
- var processorsTableSQL = SQL15`
3702
- CREATE TABLE IF NOT EXISTS ${SQL15.identifier(processorsTable.name)}(
3703
- last_processed_transaction_id XID8 NOT NULL,
3704
- version INT NOT NULL DEFAULT 1,
3705
- processor_id TEXT NOT NULL,
3706
- partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
3707
- status TEXT NOT NULL DEFAULT 'stopped',
3708
- last_processed_checkpoint TEXT NOT NULL,
3709
- processor_instance_id TEXT DEFAULT '${SQL15.plain(unknownTag2)}',
3710
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
3711
- last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
3712
- PRIMARY KEY (processor_id, partition, version)
3713
- ) PARTITION BY LIST (partition);
3714
- `;
3715
- var projectionsTableSQL = SQL15`
3716
- CREATE TABLE IF NOT EXISTS ${SQL15.identifier(projectionsTable.name)}(
3717
- version INT NOT NULL DEFAULT 1,
3718
- type VARCHAR(1) NOT NULL,
3719
- name TEXT NOT NULL,
3720
- partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
3721
- kind TEXT NOT NULL,
3722
- status TEXT NOT NULL,
3723
- definition JSONB NOT NULL DEFAULT '{}'::jsonb,
3724
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
3725
- last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
3726
- PRIMARY KEY (name, partition, version)
3727
- ) PARTITION BY LIST (partition);
3728
- `;
3729
- var sanitizeNameSQL = createFunctionIfDoesNotExistSQL(
3730
- "emt_sanitize_name",
3731
- SQL15`CREATE OR REPLACE FUNCTION emt_sanitize_name(input_name TEXT) RETURNS TEXT AS $emt_sanitize_name$
3732
- BEGIN
3733
- RETURN REGEXP_REPLACE(input_name, '[^a-zA-Z0-9_]', '_', 'g');
3734
- END;
3735
- $emt_sanitize_name$ LANGUAGE plpgsql;`
3736
- );
3737
- var addTablePartitions = createFunctionIfDoesNotExistSQL(
3738
- "emt_add_table_partition",
3739
- SQL15`
3740
- CREATE OR REPLACE FUNCTION emt_add_table_partition(tableName TEXT, partition_name TEXT) RETURNS void AS $emt_add_table_partition$
3741
- DECLARE
3742
- v_main_partiton_name TEXT;
3743
- v_active_partiton_name TEXT;
3744
- v_archived_partiton_name TEXT;
3745
- BEGIN
3746
- v_main_partiton_name := emt_sanitize_name(tableName || '_' || partition_name);
3747
- v_active_partiton_name := emt_sanitize_name(v_main_partiton_name || '_active');
3748
- v_archived_partiton_name := emt_sanitize_name(v_main_partiton_name || '_archived');
4108
+ -- Return appropriate codes based on current position
4109
+ IF current_position = p_position THEN
4110
+ RETURN 0; -- Idempotent check: position already set
4111
+ ELSIF current_position > p_position THEN
4112
+ RETURN 3; -- Current ahead: another process has progressed further
4113
+ ELSE
4114
+ RETURN 2; -- Mismatch: check position doesn't match current
4115
+ END IF;
4116
+ END IF;
3749
4117
 
4118
+ -- Handle the case when p_check_position is NULL: Insert if not exists
4119
+ BEGIN
4120
+ INSERT INTO "${SQL13.plain(processorsTable.name)}"("processor_id", "version", "last_processed_checkpoint", "partition", "last_processed_transaction_id", "created_at", "last_updated")
4121
+ VALUES (p_processor_id, p_version, p_position, p_partition, p_transaction_id, now(), now());
4122
+ RETURN 1; -- Successfully inserted
4123
+ EXCEPTION WHEN unique_violation THEN
4124
+ -- If insertion failed, it means the row already exists
4125
+ SELECT "last_processed_checkpoint" INTO current_position
4126
+ FROM "${SQL13.plain(processorsTable.name)}"
4127
+ WHERE "processor_id" = p_processor_id
4128
+ AND "partition" = p_partition
4129
+ AND "version" = p_version;
3750
4130
 
3751
- -- create default partition
3752
- EXECUTE format('
3753
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3754
- FOR VALUES IN (%L) PARTITION BY LIST (is_archived);',
3755
- v_main_partiton_name, tableName, partition_name
3756
- );
3757
-
3758
- EXECUTE format('
3759
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3760
- FOR VALUES IN (FALSE);',
3761
- v_active_partiton_name, v_main_partiton_name
3762
- );
3763
-
3764
- EXECUTE format('
3765
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3766
- FOR VALUES IN (TRUE);',
3767
- v_archived_partiton_name, v_main_partiton_name
3768
- );
4131
+ IF current_position = p_position THEN
4132
+ RETURN 0; -- Idempotent check: position already set
4133
+ ELSIF current_position > p_position THEN
4134
+ RETURN 3; -- Current ahead: another process has progressed further
4135
+ ELSE
4136
+ RETURN 2; -- Insertion failed, row already exists with different position
4137
+ END IF;
3769
4138
  END;
3770
- $emt_add_table_partition$ LANGUAGE plpgsql;`
4139
+ END;
4140
+ $spc$ LANGUAGE plpgsql;
4141
+ `
3771
4142
  );
3772
- var addPartitionSQL = createFunctionIfDoesNotExistSQL(
3773
- "emt_add_partition",
3774
- SQL15`
3775
- CREATE OR REPLACE FUNCTION emt_add_partition(partition_name TEXT) RETURNS void AS $emt_add_partition$
3776
- BEGIN
3777
- PERFORM emt_add_table_partition('${SQL15.plain(messagesTable.name)}', partition_name);
3778
- PERFORM emt_add_table_partition('${SQL15.plain(streamsTable.name)}', partition_name);
4143
+ var callStoreProcessorCheckpoint = (params) => SQL13`
4144
+ SELECT store_processor_checkpoint(
4145
+ ${params.processorId},
4146
+ ${params.version},
4147
+ ${params.position},
4148
+ ${params.checkPosition},
4149
+ pg_current_xact_id(),
4150
+ ${params.partition},
4151
+ ${params.processorInstanceId}
4152
+ ) as result;`;
4153
+ var storeProcessorCheckpoint = async (execute, options) => {
4154
+ try {
4155
+ const { result } = await single5(
4156
+ execute.command(
4157
+ callStoreProcessorCheckpoint({
4158
+ processorId: options.processorId,
4159
+ version: options.version ?? 1,
4160
+ position: options.newCheckpoint !== null ? options.newCheckpoint : null,
4161
+ checkPosition: options.lastProcessedCheckpoint !== null ? options.lastProcessedCheckpoint : null,
4162
+ partition: options.partition ?? defaultTag2,
4163
+ processorInstanceId: options.processorInstanceId ?? unknownTag2
4164
+ })
4165
+ )
4166
+ );
4167
+ return result === 1 ? { success: true, newCheckpoint: options.newCheckpoint } : {
4168
+ success: false,
4169
+ reason: result === 0 ? "IGNORED" : result === 3 ? "CURRENT_AHEAD" : "MISMATCH"
4170
+ };
4171
+ } catch (error) {
4172
+ console.log(error);
4173
+ throw error;
4174
+ }
4175
+ };
3779
4176
 
3780
- EXECUTE format('
3781
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3782
- FOR VALUES IN (%L);',
3783
- emt_sanitize_name('${SQL15.plain(processorsTable.name)}' || '_' || partition_name), '${SQL15.plain(processorsTable.name)}', partition_name
3784
- );
4177
+ // src/eventStore/schema/tables.ts
4178
+ import { SQL as SQL15 } from "@event-driven-io/dumbo";
3785
4179
 
3786
- EXECUTE format('
3787
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3788
- FOR VALUES IN (%L);',
3789
- emt_sanitize_name('${SQL15.plain(projectionsTable.name)}' || '_' || partition_name), '${SQL15.plain(projectionsTable.name)}', partition_name
3790
- );
3791
- END;
3792
- $emt_add_partition$ LANGUAGE plpgsql;`
3793
- );
3794
- var addModuleSQL = SQL15`
3795
- CREATE OR REPLACE FUNCTION add_module(new_module TEXT) RETURNS void AS $$
3796
- BEGIN
3797
- -- For ${SQL15.plain(messagesTable.name)} table
3798
- EXECUTE format('
3799
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3800
- FOR VALUES IN (emt_sanitize_name(%L || ''__'' || %L)) PARTITION BY LIST (is_archived);',
3801
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}'), '${SQL15.plain(messagesTable.name)}', new_module, '${SQL15.plain(globalTag)}'
3802
- );
3803
-
3804
- EXECUTE format('
3805
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3806
- FOR VALUES IN (FALSE);',
3807
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
3808
- );
3809
-
3810
- EXECUTE format('
3811
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3812
- FOR VALUES IN (TRUE);',
3813
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
3814
- );
3815
-
3816
- -- For ${SQL15.plain(streamsTable.name)} table
3817
- EXECUTE format('
3818
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3819
- FOR VALUES IN (emt_sanitize_name(%L || ''__'' || %L)) PARTITION BY LIST (is_archived);',
3820
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}'), '${SQL15.plain(streamsTable.name)}', new_module, '${SQL15.plain(globalTag)}'
3821
- );
3822
-
3823
- EXECUTE format('
3824
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3825
- FOR VALUES IN (FALSE);',
3826
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
3827
- );
3828
-
3829
- EXECUTE format('
3830
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3831
- FOR VALUES IN (TRUE);',
3832
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
3833
- );
3834
- END;
3835
- $$ LANGUAGE plpgsql;
3836
- `;
3837
- var addTenantSQL = SQL15`
3838
- CREATE OR REPLACE FUNCTION add_tenant(new_module TEXT, new_tenant TEXT) RETURNS void AS $$
3839
- BEGIN
3840
- -- For ${SQL15.plain(messagesTable.name)} table
3841
- EXECUTE format('
3842
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3843
- FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
3844
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant), '${SQL15.plain(messagesTable.name)}', new_module, new_tenant
3845
- );
3846
-
3847
- EXECUTE format('
3848
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3849
- FOR VALUES IN (FALSE);',
3850
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant)
3851
- );
3852
-
3853
- EXECUTE format('
3854
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3855
- FOR VALUES IN (TRUE);',
3856
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant)
3857
- );
3858
-
3859
- -- For ${SQL15.plain(streamsTable.name)} table
4180
+ // src/eventStore/schema/migrations/0_43_0/index.ts
4181
+ import {
4182
+ dumbo as dumbo2,
4183
+ SQL as SQL14,
4184
+ sqlMigration as sqlMigration3
4185
+ } from "@event-driven-io/dumbo";
4186
+ var migration_0_43_0_cleanupLegacySubscriptionSQL = SQL14`
4187
+ DO $$
4188
+ BEGIN
4189
+ IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'emt_subscriptions') THEN
4190
+ -- Restore clean emt_add_partition (remove creation of emt_subscriptions partitions)
4191
+ CREATE OR REPLACE FUNCTION emt_add_partition(partition_name TEXT) RETURNS void AS $fnpar$
4192
+ BEGIN
4193
+ PERFORM emt_add_table_partition('${SQL14.plain(messagesTable.name)}', partition_name);
4194
+ PERFORM emt_add_table_partition('${SQL14.plain(streamsTable.name)}', partition_name);
4195
+
3860
4196
  EXECUTE format('
3861
4197
  CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3862
- FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
3863
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant), '${SQL15.plain(streamsTable.name)}', new_module, new_tenant
3864
- );
3865
-
3866
- EXECUTE format('
3867
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3868
- FOR VALUES IN (FALSE);',
3869
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant)
3870
- );
3871
-
3872
- EXECUTE format('
3873
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3874
- FOR VALUES IN (TRUE);',
3875
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant)
3876
- );
3877
- END;
3878
- $$ LANGUAGE plpgsql;
3879
- `;
3880
- var addModuleForAllTenantsSQL = SQL15`
3881
- CREATE OR REPLACE FUNCTION add_module_for_all_tenants(new_module TEXT) RETURNS void AS $$
3882
- DECLARE
3883
- tenant_record RECORD;
3884
- BEGIN
3885
- PERFORM add_module(new_module);
3886
-
3887
- FOR tenant_record IN SELECT DISTINCT tenant FROM ${SQL15.plain(messagesTable.name)}
3888
- LOOP
3889
- -- For ${SQL15.plain(messagesTable.name)} table
3890
- EXECUTE format('
3891
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3892
- FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
3893
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant), '${SQL15.plain(messagesTable.name)}', new_module, tenant_record.tenant
3894
- );
3895
-
3896
- EXECUTE format('
3897
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3898
- FOR VALUES IN (FALSE);',
3899
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant)
3900
- );
3901
-
3902
- EXECUTE format('
3903
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3904
- FOR VALUES IN (TRUE);',
3905
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant)
3906
- );
3907
-
3908
- -- For ${SQL15.plain(streamsTable.name)} table
3909
- EXECUTE format('
3910
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3911
- FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
3912
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant), '${SQL15.plain(streamsTable.name)}', new_module, tenant_record.tenant
3913
- );
3914
-
3915
- EXECUTE format('
3916
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3917
- FOR VALUES IN (FALSE);',
3918
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant)
3919
- );
3920
-
3921
- EXECUTE format('
3922
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3923
- FOR VALUES IN (TRUE);',
3924
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant)
3925
- );
3926
- END LOOP;
3927
- END;
3928
- $$ LANGUAGE plpgsql;
3929
- `;
3930
- var addTenantForAllModulesSQL = SQL15`
3931
- CREATE OR REPLACE FUNCTION add_tenant_for_all_modules(new_tenant TEXT) RETURNS void AS $$
3932
- DECLARE
3933
- module_record RECORD;
3934
- BEGIN
3935
- FOR module_record IN SELECT DISTINCT partitionname FROM pg_partman.part_config WHERE parent_table = '${SQL15.plain(messagesTable.name)}'
3936
- LOOP
3937
- -- For ${SQL15.plain(messagesTable.name)} table
3938
- EXECUTE format('
3939
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3940
- FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
3941
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant), '${SQL15.plain(messagesTable.name)}', module_record.partitionname, new_tenant
3942
- );
3943
-
3944
- EXECUTE format('
3945
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3946
- FOR VALUES IN (FALSE);',
3947
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant)
3948
- );
3949
-
3950
- EXECUTE format('
3951
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3952
- FOR VALUES IN (TRUE);',
3953
- emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant)
3954
- );
3955
-
3956
- -- For ${SQL15.plain(streamsTable.name)} table
3957
- EXECUTE format('
3958
- CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
3959
- FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
3960
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant), '${SQL15.plain(streamsTable.name)}', module_record.partitionname, new_tenant
3961
- );
3962
-
3963
- EXECUTE format('
3964
- CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
3965
- FOR VALUES IN (FALSE);',
3966
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant)
3967
- );
3968
-
3969
- EXECUTE format('
3970
- CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
3971
- FOR VALUES IN (TRUE);',
3972
- emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant)
3973
- );
3974
- END LOOP;
4198
+ FOR VALUES IN (%L);',
4199
+ emt_sanitize_name('${SQL14.plain(processorsTable.name)}' || '_' || partition_name), '${SQL14.plain(processorsTable.name)}', partition_name
4200
+ );
4201
+
4202
+ EXECUTE format('
4203
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4204
+ FOR VALUES IN (%L);',
4205
+ emt_sanitize_name('${SQL14.plain(projectionsTable.name)}' || '_' || partition_name), '${SQL14.plain(projectionsTable.name)}', partition_name
4206
+ );
3975
4207
  END;
3976
- $$ LANGUAGE plpgsql;
3977
- `;
3978
- var addDefaultPartitionSQL = SQL15`SELECT emt_add_partition('${SQL15.plain(defaultTag2)}');`;
4208
+ $fnpar$ LANGUAGE plpgsql;
3979
4209
 
3980
- // src/eventStore/schema/readProcessorCheckpoint.ts
3981
- import { singleOrNull as singleOrNull3, SQL as SQL16 } from "@event-driven-io/dumbo";
3982
- var readProcessorCheckpoint = async (execute, options) => {
3983
- const result = await singleOrNull3(
3984
- execute.query(
3985
- SQL16`SELECT last_processed_checkpoint
3986
- FROM ${SQL16.identifier(processorsTable.name)}
3987
- WHERE partition = ${options?.partition ?? defaultTag2} AND processor_id = ${options.processorId} AND version = ${options.version ?? 1}
3988
- LIMIT 1`
3989
- )
3990
- );
3991
- return {
3992
- lastProcessedCheckpoint: result !== null ? result.last_processed_checkpoint : null
3993
- };
3994
- };
4210
+ -- Drop old subscriptions table if it exists
4211
+ DROP TABLE IF EXISTS emt_subscriptions CASCADE;
3995
4212
 
3996
- // src/eventStore/schema/readStream.ts
3997
- import { mapRows as mapRows2, SQL as SQL17 } from "@event-driven-io/dumbo";
3998
- var readStream = async (execute, streamId, options) => {
3999
- const fromCondition = options?.from ? `AND stream_position >= ${options.from}` : "";
4000
- const to = Number(
4001
- options?.to ?? (options?.maxCount ? (options.from ?? 0n) + options.maxCount : NaN)
4002
- );
4003
- const toCondition = !isNaN(to) ? `AND stream_position <= ${to}` : "";
4004
- const events = await mapRows2(
4005
- execute.query(
4006
- SQL17`SELECT stream_id, stream_position, global_position, message_data, message_metadata, message_schema_version, message_type, message_id
4007
- FROM ${SQL17.identifier(messagesTable.name)}
4008
- WHERE stream_id = ${streamId} AND partition = ${options?.partition ?? defaultTag2} AND is_archived = FALSE ${SQL17.plain(fromCondition)} ${SQL17.plain(toCondition)}
4009
- ORDER BY stream_position ASC`
4010
- ),
4011
- (row) => {
4012
- const rawEvent = {
4013
- type: row.message_type,
4014
- data: row.message_data,
4015
- metadata: row.message_metadata
4016
- };
4017
- const metadata = {
4018
- ..."metadata" in rawEvent ? rawEvent.metadata ?? {} : {},
4019
- messageId: row.message_id,
4020
- streamName: streamId,
4021
- streamPosition: BigInt(row.stream_position),
4022
- globalPosition: BigInt(row.global_position),
4023
- checkpoint: bigIntProcessorCheckpoint(BigInt(row.global_position))
4024
- };
4025
- const event = {
4026
- ...rawEvent,
4027
- kind: "Event",
4028
- metadata
4029
- };
4030
- return upcastRecordedMessage(event, options?.schema?.versioning);
4031
- }
4032
- );
4033
- return events.length > 0 ? {
4034
- currentStreamVersion: events[events.length - 1].metadata.streamPosition,
4035
- events,
4036
- streamExists: true
4037
- } : {
4038
- currentStreamVersion: PostgreSQLEventStoreDefaultStreamVersion,
4039
- events: [],
4040
- streamExists: false
4041
- };
4042
- };
4213
+ -- Drop old function if it exists
4214
+ DROP FUNCTION IF EXISTS store_subscription_checkpoint(character varying, bigint, bigint, bigint, xid8, text);
4215
+
4216
+ -- Restore clean store_processor_checkpoint (remove dual-write logic)
4217
+ CREATE OR REPLACE FUNCTION store_processor_checkpoint(
4218
+ p_processor_id TEXT,
4219
+ p_version BIGINT,
4220
+ p_position TEXT,
4221
+ p_check_position TEXT,
4222
+ p_transaction_id xid8,
4223
+ p_partition TEXT DEFAULT '${SQL14.plain(defaultTag2)}',
4224
+ p_processor_instance_id TEXT DEFAULT 'emt:unknown'
4225
+ ) RETURNS INT AS $fn$
4226
+ DECLARE
4227
+ current_position TEXT;
4228
+ BEGIN
4229
+ IF p_check_position IS NOT NULL THEN
4230
+ UPDATE "emt_processors"
4231
+ SET
4232
+ "last_processed_checkpoint" = p_position,
4233
+ "last_processed_transaction_id" = p_transaction_id,
4234
+ "last_updated" = now()
4235
+ WHERE "processor_id" = p_processor_id
4236
+ AND "last_processed_checkpoint" = p_check_position
4237
+ AND "partition" = p_partition
4238
+ AND "version" = p_version;
4043
4239
 
4044
- // src/eventStore/schema/streamExists.ts
4045
- import { SQL as SQL18 } from "@event-driven-io/dumbo";
4046
- var streamExists = async (execute, streamId, options) => {
4047
- const queryResult = await execute.query(
4048
- SQL18`SELECT EXISTS (
4049
- SELECT 1
4050
- from ${SQL18.identifier(streamsTable.name)}
4051
- WHERE stream_id = ${streamId} AND partition = ${options?.partition ?? defaultTag2} AND is_archived = FALSE)
4052
- `
4053
- );
4054
- return queryResult.rows[0]?.exists ?? false;
4240
+ IF FOUND THEN
4241
+ RETURN 1;
4242
+ END IF;
4243
+
4244
+ SELECT "last_processed_checkpoint" INTO current_position
4245
+ FROM "emt_processors"
4246
+ WHERE "processor_id" = p_processor_id
4247
+ AND "partition" = p_partition
4248
+ AND "version" = p_version ;
4249
+
4250
+ IF current_position = p_position THEN
4251
+ RETURN 0;
4252
+ ELSIF current_position > p_position THEN
4253
+ RETURN 3;
4254
+ ELSE
4255
+ RETURN 2;
4256
+ END IF;
4257
+ END IF;
4258
+
4259
+ BEGIN
4260
+ INSERT INTO "emt_processors"("processor_id", "version", "last_processed_checkpoint", "partition", "last_processed_transaction_id", "created_at", "last_updated")
4261
+ VALUES (p_processor_id, p_version, p_position, p_partition, p_transaction_id, now(), now());
4262
+ RETURN 1;
4263
+ EXCEPTION WHEN unique_violation THEN
4264
+ SELECT "last_processed_checkpoint" INTO current_position
4265
+ FROM "emt_processors"
4266
+ WHERE "processor_id" = p_processor_id
4267
+ AND "partition" = p_partition
4268
+ AND "version" = p_version;
4269
+
4270
+ IF current_position = p_position THEN
4271
+ RETURN 0;
4272
+ ELSE
4273
+ RETURN 2;
4274
+ END IF;
4275
+ END;
4276
+ END;
4277
+ $fn$ LANGUAGE plpgsql;
4278
+ END IF;
4279
+ END $$;
4280
+ `;
4281
+ var migration_0_43_0_cleanupLegacySubscription = sqlMigration3("emt:postgresql:eventstore:0.43.0:cleanup-legacy-subscription", [
4282
+ migration_0_43_0_cleanupLegacySubscriptionSQL
4283
+ ]);
4284
+ var cleanupLegacySubscriptionTables = async (connectionString) => {
4285
+ const pool = dumbo2({ connectionString });
4286
+ try {
4287
+ await pool.withTransaction(async ({ execute }) => {
4288
+ await execute.command(migration_0_43_0_cleanupLegacySubscriptionSQL);
4289
+ });
4290
+ } finally {
4291
+ await pool.close();
4292
+ }
4055
4293
  };
4056
4294
 
4057
- // src/eventStore/schema/index.ts
4058
- var schemaSQL = [
4059
- streamsTableSQL,
4060
- messagesTableSQL,
4061
- projectionsTableSQL,
4062
- processorsTableSQL,
4063
- sanitizeNameSQL,
4064
- addTablePartitions,
4065
- addPartitionSQL,
4066
- appendToStreamSQL,
4067
- addDefaultPartitionSQL,
4068
- storeSubscriptionCheckpointSQL,
4069
- tryAcquireProcessorLockSQL,
4070
- releaseProcessorLockSQL,
4071
- registerProjectionSQL,
4072
- activateProjectionSQL,
4073
- deactivateProjectionSQL
4074
- ];
4075
- var schemaMigration = sqlMigration4(
4076
- "emt:postgresql:eventstore:initial",
4077
- schemaSQL
4295
+ // src/eventStore/schema/tables.ts
4296
+ var streamsTableSQL = SQL15`
4297
+ CREATE TABLE IF NOT EXISTS ${SQL15.identifier(streamsTable.name)}(
4298
+ stream_id TEXT NOT NULL,
4299
+ stream_position BIGINT NOT NULL,
4300
+ partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
4301
+ stream_type TEXT NOT NULL,
4302
+ stream_metadata JSONB NOT NULL,
4303
+ is_archived BOOLEAN NOT NULL DEFAULT FALSE,
4304
+ PRIMARY KEY (stream_id, partition, is_archived)
4305
+ ) PARTITION BY LIST (partition);
4306
+
4307
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_streams_unique
4308
+ ON ${SQL15.identifier(streamsTable.name)}(stream_id, partition, is_archived)
4309
+ INCLUDE (stream_position);`;
4310
+ var messagesTableSQL = SQL15`
4311
+ CREATE SEQUENCE IF NOT EXISTS emt_global_message_position;
4312
+
4313
+ CREATE TABLE IF NOT EXISTS ${SQL15.identifier(messagesTable.name)}(
4314
+ stream_position BIGINT NOT NULL,
4315
+ global_position BIGINT DEFAULT nextval('emt_global_message_position'),
4316
+ transaction_id XID8 NOT NULL,
4317
+ created TIMESTAMPTZ NOT NULL DEFAULT now(),
4318
+ is_archived BOOLEAN NOT NULL DEFAULT FALSE,
4319
+ message_kind VARCHAR(1) NOT NULL DEFAULT 'E',
4320
+ stream_id TEXT NOT NULL,
4321
+ partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
4322
+ message_schema_version TEXT NOT NULL,
4323
+ message_id TEXT NOT NULL,
4324
+ message_type TEXT NOT NULL,
4325
+ message_data JSONB NOT NULL,
4326
+ message_metadata JSONB NOT NULL,
4327
+ PRIMARY KEY (stream_id, stream_position, partition, is_archived)
4328
+ ) PARTITION BY LIST (partition);`;
4329
+ var processorsTableSQL = SQL15`
4330
+ CREATE TABLE IF NOT EXISTS ${SQL15.identifier(processorsTable.name)}(
4331
+ last_processed_transaction_id XID8 NOT NULL,
4332
+ version INT NOT NULL DEFAULT 1,
4333
+ processor_id TEXT NOT NULL,
4334
+ partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
4335
+ status TEXT NOT NULL DEFAULT 'stopped',
4336
+ last_processed_checkpoint TEXT NOT NULL,
4337
+ processor_instance_id TEXT DEFAULT '${SQL15.plain(unknownTag2)}',
4338
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
4339
+ last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
4340
+ PRIMARY KEY (processor_id, partition, version)
4341
+ ) PARTITION BY LIST (partition);
4342
+ `;
4343
+ var projectionsTableSQL = SQL15`
4344
+ CREATE TABLE IF NOT EXISTS ${SQL15.identifier(projectionsTable.name)}(
4345
+ version INT NOT NULL DEFAULT 1,
4346
+ type VARCHAR(1) NOT NULL,
4347
+ name TEXT NOT NULL,
4348
+ partition TEXT NOT NULL DEFAULT '${SQL15.plain(defaultTag2)}',
4349
+ kind TEXT NOT NULL,
4350
+ status TEXT NOT NULL,
4351
+ definition JSONB NOT NULL DEFAULT '{}'::jsonb,
4352
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
4353
+ last_updated TIMESTAMPTZ NOT NULL DEFAULT now(),
4354
+ PRIMARY KEY (name, partition, version)
4355
+ ) PARTITION BY LIST (partition);
4356
+ `;
4357
+ var sanitizeNameSQL = createFunctionIfDoesNotExistSQL(
4358
+ "emt_sanitize_name",
4359
+ SQL15`CREATE OR REPLACE FUNCTION emt_sanitize_name(input_name TEXT) RETURNS TEXT AS $emt_sanitize_name$
4360
+ BEGIN
4361
+ RETURN REGEXP_REPLACE(input_name, '[^a-zA-Z0-9_]', '_', 'g');
4362
+ END;
4363
+ $emt_sanitize_name$ LANGUAGE plpgsql;`
4078
4364
  );
4079
- var eventStoreSchemaMigrations = [
4080
- migration_0_38_7_and_older,
4081
- migration_0_42_0_FromSubscriptionsToProcessors,
4082
- migration_0_42_0_2_AddProcessorProjectionFunctions,
4083
- schemaMigration
4084
- ];
4085
- var createEventStoreSchema = (connectionString, pool, hooks, options) => {
4086
- return pool.withTransaction(async (tx) => {
4087
- const context = await transactionToPostgreSQLProjectionHandlerContext(
4088
- connectionString,
4089
- pool,
4090
- tx
4091
- );
4092
- const nestedPool = dumbo2({ connectionString, connection: tx.connection });
4093
- try {
4094
- if (hooks?.onBeforeSchemaCreated) {
4095
- await hooks.onBeforeSchemaCreated(context);
4096
- }
4097
- const result = await runSQLMigrations(
4098
- nestedPool,
4099
- eventStoreSchemaMigrations,
4100
- options
4101
- );
4102
- if (hooks?.onAfterSchemaCreated) {
4103
- await hooks.onAfterSchemaCreated(context);
4104
- }
4105
- return result;
4106
- } finally {
4107
- await nestedPool.close();
4108
- }
4109
- });
4110
- };
4365
+ var addTablePartitions = createFunctionIfDoesNotExistSQL(
4366
+ "emt_add_table_partition",
4367
+ SQL15`
4368
+ CREATE OR REPLACE FUNCTION emt_add_table_partition(tableName TEXT, partition_name TEXT) RETURNS void AS $emt_add_table_partition$
4369
+ DECLARE
4370
+ v_main_partiton_name TEXT;
4371
+ v_active_partiton_name TEXT;
4372
+ v_archived_partiton_name TEXT;
4373
+ BEGIN
4374
+ v_main_partiton_name := emt_sanitize_name(tableName || '_' || partition_name);
4375
+ v_active_partiton_name := emt_sanitize_name(v_main_partiton_name || '_active');
4376
+ v_archived_partiton_name := emt_sanitize_name(v_main_partiton_name || '_archived');
4111
4377
 
4112
- // src/eventStore/schema/truncateTables.ts
4113
- import { SQL as SQL19 } from "@event-driven-io/dumbo";
4114
- var truncateTables = async (execute, options) => {
4115
- await execute.command(
4116
- SQL19`TRUNCATE TABLE
4117
- ${SQL19.identifier(streamsTable.name)},
4118
- ${SQL19.identifier(messagesTable.name)},
4119
- ${SQL19.identifier(processorsTable.name)},
4120
- ${SQL19.identifier(projectionsTable.name)}
4121
- CASCADE${SQL19.plain(options?.resetSequences ? "; ALTER SEQUENCE emt_global_message_position RESTART WITH 1" : "")};`
4122
- );
4123
- };
4124
4378
 
4125
- // src/eventStore/postgreSQLEventStore.ts
4126
- var defaultPostgreSQLOptions = {
4127
- projections: [],
4128
- schema: { autoMigration: "CreateOrUpdate" }
4129
- };
4130
- var PostgreSQLEventStoreDefaultStreamVersion = 0n;
4131
- var getPostgreSQLEventStore = (connectionString, options = defaultPostgreSQLOptions) => {
4132
- const poolOptions = {
4133
- connectionString,
4134
- ...options.connectionOptions ? options.connectionOptions : {}
4135
- };
4136
- const pool = "dumbo" in poolOptions ? poolOptions.dumbo : dumbo3(poolOptions);
4137
- let migrateSchema = void 0;
4138
- const autoGenerateSchema = options.schema?.autoMigration === void 0 || options.schema?.autoMigration !== "None";
4139
- const inlineProjections = (options.projections ?? []).filter(({ type }) => type === "inline").map(({ projection: projection2 }) => projection2);
4140
- const migrate = async (migrationOptions) => {
4141
- if (!migrateSchema) {
4142
- migrateSchema = createEventStoreSchema(
4143
- connectionString,
4144
- pool,
4145
- {
4146
- onBeforeSchemaCreated: async (context) => {
4147
- if (options.hooks?.onBeforeSchemaCreated) {
4148
- await options.hooks.onBeforeSchemaCreated(context);
4149
- }
4150
- },
4151
- onAfterSchemaCreated: async (context) => {
4152
- for (const projection2 of inlineProjections) {
4153
- if (projection2.init) {
4154
- await projection2.init({
4155
- version: projection2.version ?? 1,
4156
- status: "active",
4157
- registrationType: "inline",
4158
- context: { ...context, migrationOptions }
4159
- });
4160
- }
4161
- }
4162
- if (options.hooks?.onAfterSchemaCreated) {
4163
- await options.hooks.onAfterSchemaCreated(context);
4164
- }
4165
- }
4166
- },
4167
- migrationOptions
4379
+ -- create default partition
4380
+ EXECUTE format('
4381
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4382
+ FOR VALUES IN (%L) PARTITION BY LIST (is_archived);',
4383
+ v_main_partiton_name, tableName, partition_name
4168
4384
  );
4169
- }
4170
- return migrateSchema;
4171
- };
4172
- const ensureSchemaExists = () => {
4173
- if (!autoGenerateSchema) return Promise.resolve();
4174
- return migrate();
4175
- };
4176
- const beforeCommitHook = inlineProjections.length > 0 ? async (events, { transaction }) => handleProjections({
4177
- projections: inlineProjections,
4178
- // TODO: Add proper handling of global data
4179
- // Currently it's not available as append doesn't return array of global position but just the last one
4180
- events,
4181
- ...await transactionToPostgreSQLProjectionHandlerContext(
4182
- connectionString,
4183
- pool,
4184
- transaction
4185
- )
4186
- }) : void 0;
4187
- return {
4188
- schema: {
4189
- sql: () => SQL20.describe(
4190
- schemaSQL,
4191
- getFormatter(fromDatabaseDriverType(pool.driverType).databaseType)
4192
- ),
4193
- print: () => console.log(
4194
- SQL20.describe(
4195
- schemaSQL,
4196
- getFormatter(fromDatabaseDriverType(pool.driverType).databaseType)
4197
- )
4198
- ),
4199
- migrate,
4200
- dangerous: {
4201
- truncate: (truncateOptions) => pool.withTransaction(async (transaction) => {
4202
- await ensureSchemaExists();
4203
- await truncateTables(transaction.execute, truncateOptions);
4204
- if (truncateOptions?.truncateProjections) {
4205
- const projectionContext = await transactionToPostgreSQLProjectionHandlerContext(
4206
- connectionString,
4207
- pool,
4208
- transaction
4209
- );
4210
- for (const projection2 of options?.projections ?? []) {
4211
- if (projection2.projection.truncate)
4212
- await projection2.projection.truncate(projectionContext);
4213
- }
4214
- }
4215
- })
4216
- }
4217
- },
4218
- async aggregateStream(streamName, options2) {
4219
- const { evolve, initialState, read } = options2;
4220
- const expectedStreamVersion = read?.expectedStreamVersion;
4221
- let state = initialState();
4222
- const result = await this.readStream(
4223
- streamName,
4224
- read
4385
+
4386
+ EXECUTE format('
4387
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4388
+ FOR VALUES IN (FALSE);',
4389
+ v_active_partiton_name, v_main_partiton_name
4225
4390
  );
4226
- const currentStreamVersion = result.currentStreamVersion;
4227
- assertExpectedVersionMatchesCurrent(
4228
- currentStreamVersion,
4229
- expectedStreamVersion,
4230
- PostgreSQLEventStoreDefaultStreamVersion
4391
+
4392
+ EXECUTE format('
4393
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4394
+ FOR VALUES IN (TRUE);',
4395
+ v_archived_partiton_name, v_main_partiton_name
4231
4396
  );
4232
- for (const event of result.events) {
4233
- if (!event) continue;
4234
- state = evolve(state, event);
4235
- }
4236
- return {
4237
- currentStreamVersion,
4238
- state,
4239
- streamExists: result.streamExists
4240
- };
4241
- },
4242
- readStream: async (streamName, options2) => {
4243
- await ensureSchemaExists();
4244
- return readStream(
4245
- pool.execute,
4246
- streamName,
4247
- options2
4397
+ END;
4398
+ $emt_add_table_partition$ LANGUAGE plpgsql;`
4399
+ );
4400
+ var addPartitionSQL = createFunctionIfDoesNotExistSQL(
4401
+ "emt_add_partition",
4402
+ SQL15`
4403
+ CREATE OR REPLACE FUNCTION emt_add_partition(partition_name TEXT) RETURNS void AS $emt_add_partition$
4404
+ BEGIN
4405
+ PERFORM emt_add_table_partition('${SQL15.plain(messagesTable.name)}', partition_name);
4406
+ PERFORM emt_add_table_partition('${SQL15.plain(streamsTable.name)}', partition_name);
4407
+
4408
+ EXECUTE format('
4409
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4410
+ FOR VALUES IN (%L);',
4411
+ emt_sanitize_name('${SQL15.plain(processorsTable.name)}' || '_' || partition_name), '${SQL15.plain(processorsTable.name)}', partition_name
4248
4412
  );
4249
- },
4250
- appendToStream: async (streamName, events, options2) => {
4251
- await ensureSchemaExists();
4252
- const [firstPart, ...rest] = streamName.split("-");
4253
- const streamType = firstPart && rest.length > 0 ? firstPart : unknownTag2;
4254
- const appendResult = await appendToStream(
4255
- // TODO: Fix this when introducing more drivers
4256
- pool,
4257
- streamName,
4258
- streamType,
4259
- downcastRecordedMessages(events, options2?.schema?.versioning),
4260
- {
4261
- ...options2,
4262
- beforeCommitHook
4263
- }
4413
+
4414
+ EXECUTE format('
4415
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4416
+ FOR VALUES IN (%L);',
4417
+ emt_sanitize_name('${SQL15.plain(projectionsTable.name)}' || '_' || partition_name), '${SQL15.plain(projectionsTable.name)}', partition_name
4264
4418
  );
4265
- if (!appendResult.success)
4266
- throw new ExpectedVersionConflictError(
4267
- -1n,
4268
- //TODO: Return actual version in case of error
4269
- options2?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK
4419
+ END;
4420
+ $emt_add_partition$ LANGUAGE plpgsql;`
4421
+ );
4422
+ var addModuleSQL = SQL15`
4423
+ CREATE OR REPLACE FUNCTION add_module(new_module TEXT) RETURNS void AS $$
4424
+ BEGIN
4425
+ -- For ${SQL15.plain(messagesTable.name)} table
4426
+ EXECUTE format('
4427
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4428
+ FOR VALUES IN (emt_sanitize_name(%L || ''__'' || %L)) PARTITION BY LIST (is_archived);',
4429
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}'), '${SQL15.plain(messagesTable.name)}', new_module, '${SQL15.plain(globalTag)}'
4430
+ );
4431
+
4432
+ EXECUTE format('
4433
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4434
+ FOR VALUES IN (FALSE);',
4435
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
4436
+ );
4437
+
4438
+ EXECUTE format('
4439
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4440
+ FOR VALUES IN (TRUE);',
4441
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
4442
+ );
4443
+
4444
+ -- For ${SQL15.plain(streamsTable.name)} table
4445
+ EXECUTE format('
4446
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4447
+ FOR VALUES IN (emt_sanitize_name(%L || ''__'' || %L)) PARTITION BY LIST (is_archived);',
4448
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}'), '${SQL15.plain(streamsTable.name)}', new_module, '${SQL15.plain(globalTag)}'
4449
+ );
4450
+
4451
+ EXECUTE format('
4452
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4453
+ FOR VALUES IN (FALSE);',
4454
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
4455
+ );
4456
+
4457
+ EXECUTE format('
4458
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4459
+ FOR VALUES IN (TRUE);',
4460
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}' || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || '${SQL15.plain(globalTag)}')
4461
+ );
4462
+ END;
4463
+ $$ LANGUAGE plpgsql;
4464
+ `;
4465
+ var addTenantSQL = SQL15`
4466
+ CREATE OR REPLACE FUNCTION add_tenant(new_module TEXT, new_tenant TEXT) RETURNS void AS $$
4467
+ BEGIN
4468
+ -- For ${SQL15.plain(messagesTable.name)} table
4469
+ EXECUTE format('
4470
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4471
+ FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
4472
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant), '${SQL15.plain(messagesTable.name)}', new_module, new_tenant
4270
4473
  );
4271
- return {
4272
- nextExpectedStreamVersion: appendResult.nextStreamPosition,
4273
- lastEventGlobalPosition: appendResult.globalPositions[appendResult.globalPositions.length - 1],
4274
- createdNewStream: appendResult.nextStreamPosition >= BigInt(events.length)
4275
- };
4276
- },
4277
- streamExists: async (streamName, options2) => {
4278
- await ensureSchemaExists();
4279
- return streamExists(pool.execute, streamName, options2);
4280
- },
4281
- consumer: (options2) => postgreSQLEventStoreConsumer({
4282
- ...options2 ?? {},
4283
- pool,
4284
- connectionString
4285
- }),
4286
- close: () => pool.close(),
4287
- async withSession(callback) {
4288
- return await pool.withConnection(async (connection) => {
4289
- const storeOptions = {
4290
- ...options,
4291
- connectionOptions: {
4292
- connection
4293
- },
4294
- schema: {
4295
- ...options.schema ?? {},
4296
- autoMigration: "None"
4297
- }
4298
- };
4299
- const eventStore = getPostgreSQLEventStore(
4300
- connectionString,
4301
- storeOptions
4474
+
4475
+ EXECUTE format('
4476
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4477
+ FOR VALUES IN (FALSE);',
4478
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant)
4302
4479
  );
4303
- return ensureSchemaExists().then(
4304
- () => callback({
4305
- eventStore,
4306
- close: () => Promise.resolve()
4307
- })
4480
+
4481
+ EXECUTE format('
4482
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4483
+ FOR VALUES IN (TRUE);',
4484
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || new_tenant)
4308
4485
  );
4309
- });
4310
- }
4486
+
4487
+ -- For ${SQL15.plain(streamsTable.name)} table
4488
+ EXECUTE format('
4489
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4490
+ FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
4491
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant), '${SQL15.plain(streamsTable.name)}', new_module, new_tenant
4492
+ );
4493
+
4494
+ EXECUTE format('
4495
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4496
+ FOR VALUES IN (FALSE);',
4497
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant)
4498
+ );
4499
+
4500
+ EXECUTE format('
4501
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4502
+ FOR VALUES IN (TRUE);',
4503
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || new_tenant)
4504
+ );
4505
+ END;
4506
+ $$ LANGUAGE plpgsql;
4507
+ `;
4508
+ var addModuleForAllTenantsSQL = SQL15`
4509
+ CREATE OR REPLACE FUNCTION add_module_for_all_tenants(new_module TEXT) RETURNS void AS $$
4510
+ DECLARE
4511
+ tenant_record RECORD;
4512
+ BEGIN
4513
+ PERFORM add_module(new_module);
4514
+
4515
+ FOR tenant_record IN SELECT DISTINCT tenant FROM ${SQL15.plain(messagesTable.name)}
4516
+ LOOP
4517
+ -- For ${SQL15.plain(messagesTable.name)} table
4518
+ EXECUTE format('
4519
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4520
+ FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
4521
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant), '${SQL15.plain(messagesTable.name)}', new_module, tenant_record.tenant
4522
+ );
4523
+
4524
+ EXECUTE format('
4525
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4526
+ FOR VALUES IN (FALSE);',
4527
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant)
4528
+ );
4529
+
4530
+ EXECUTE format('
4531
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4532
+ FOR VALUES IN (TRUE);',
4533
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || new_module || '__' || tenant_record.tenant)
4534
+ );
4535
+
4536
+ -- For ${SQL15.plain(streamsTable.name)} table
4537
+ EXECUTE format('
4538
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4539
+ FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
4540
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant), '${SQL15.plain(streamsTable.name)}', new_module, tenant_record.tenant
4541
+ );
4542
+
4543
+ EXECUTE format('
4544
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4545
+ FOR VALUES IN (FALSE);',
4546
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant)
4547
+ );
4548
+
4549
+ EXECUTE format('
4550
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4551
+ FOR VALUES IN (TRUE);',
4552
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || new_module || '__' || tenant_record.tenant)
4553
+ );
4554
+ END LOOP;
4555
+ END;
4556
+ $$ LANGUAGE plpgsql;
4557
+ `;
4558
+ var addTenantForAllModulesSQL = SQL15`
4559
+ CREATE OR REPLACE FUNCTION add_tenant_for_all_modules(new_tenant TEXT) RETURNS void AS $$
4560
+ DECLARE
4561
+ module_record RECORD;
4562
+ BEGIN
4563
+ FOR module_record IN SELECT DISTINCT partitionname FROM pg_partman.part_config WHERE parent_table = '${SQL15.plain(messagesTable.name)}'
4564
+ LOOP
4565
+ -- For ${SQL15.plain(messagesTable.name)} table
4566
+ EXECUTE format('
4567
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4568
+ FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
4569
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant), '${SQL15.plain(messagesTable.name)}', module_record.partitionname, new_tenant
4570
+ );
4571
+
4572
+ EXECUTE format('
4573
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4574
+ FOR VALUES IN (FALSE);',
4575
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant)
4576
+ );
4577
+
4578
+ EXECUTE format('
4579
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4580
+ FOR VALUES IN (TRUE);',
4581
+ emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(messagesTable.name)}_' || module_record.partitionname || '__' || new_tenant)
4582
+ );
4583
+
4584
+ -- For ${SQL15.plain(streamsTable.name)} table
4585
+ EXECUTE format('
4586
+ CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
4587
+ FOR VALUES IN (emt_sanitize_name(''%s__%s'')) PARTITION BY LIST (is_archived);',
4588
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant), '${SQL15.plain(streamsTable.name)}', module_record.partitionname, new_tenant
4589
+ );
4590
+
4591
+ EXECUTE format('
4592
+ CREATE TABLE IF NOT EXISTS %I_active PARTITION OF %I
4593
+ FOR VALUES IN (FALSE);',
4594
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_active'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant)
4595
+ );
4596
+
4597
+ EXECUTE format('
4598
+ CREATE TABLE IF NOT EXISTS %I_archived PARTITION OF %I
4599
+ FOR VALUES IN (TRUE);',
4600
+ emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant || '_archived'), emt_sanitize_name('${SQL15.plain(streamsTable.name)}_' || module_record.partitionname || '__' || new_tenant)
4601
+ );
4602
+ END LOOP;
4603
+ END;
4604
+ $$ LANGUAGE plpgsql;
4605
+ `;
4606
+ var addDefaultPartitionSQL = SQL15`SELECT emt_add_partition('${SQL15.plain(defaultTag2)}');`;
4607
+
4608
+ // src/eventStore/schema/readProcessorCheckpoint.ts
4609
+ import { singleOrNull as singleOrNull3, SQL as SQL16 } from "@event-driven-io/dumbo";
4610
+ var readProcessorCheckpoint = async (execute, options) => {
4611
+ const result = await singleOrNull3(
4612
+ execute.query(
4613
+ SQL16`SELECT last_processed_checkpoint
4614
+ FROM ${SQL16.identifier(processorsTable.name)}
4615
+ WHERE partition = ${options?.partition ?? defaultTag2} AND processor_id = ${options.processorId} AND version = ${options.version ?? 1}
4616
+ LIMIT 1`
4617
+ )
4618
+ );
4619
+ return {
4620
+ lastProcessedCheckpoint: result !== null ? result.last_processed_checkpoint : null
4311
4621
  };
4312
4622
  };
4313
4623
 
4314
- // src/eventStore/projections/postgresProjectionSpec.ts
4315
- var PostgreSQLProjectionSpec = {
4316
- for: (options) => {
4317
- {
4318
- const { projection: projection2, ...dumoOptions } = options;
4319
- const { connectionString } = dumoOptions;
4320
- let wasInitialised = false;
4321
- const initialize = async (pool) => {
4322
- const eventStore = getPostgreSQLEventStore(connectionString, {
4323
- // TODO: This will need to change when we support other drivers
4324
- connectionOptions: { dumbo: pool }
4325
- });
4326
- if (wasInitialised) return;
4327
- wasInitialised = true;
4328
- await eventStore.schema.migrate();
4329
- if (projection2.init)
4330
- await pool.withTransaction(async (transaction) => {
4331
- await projection2.init({
4332
- registrationType: "async",
4333
- version: projection2.version ?? 1,
4334
- status: "active",
4335
- context: await transactionToPostgreSQLProjectionHandlerContext(
4336
- connectionString,
4337
- pool,
4338
- transaction
4339
- )
4340
- });
4341
- });
4624
+ // src/eventStore/schema/readStream.ts
4625
+ import { mapRows as mapRows2, SQL as SQL17 } from "@event-driven-io/dumbo";
4626
+ var readStream = async (execute, streamId, options) => {
4627
+ const fromCondition = options?.from ? `AND stream_position >= ${options.from}` : "";
4628
+ const to = Number(
4629
+ options?.to ?? (options?.maxCount ? (options.from ?? 0n) + options.maxCount : NaN)
4630
+ );
4631
+ const toCondition = !isNaN(to) ? `AND stream_position <= ${to}` : "";
4632
+ const events = await mapRows2(
4633
+ execute.query(
4634
+ SQL17`SELECT stream_id, stream_position, global_position, message_data, message_metadata, message_schema_version, message_type, message_id
4635
+ FROM ${SQL17.identifier(messagesTable.name)}
4636
+ WHERE stream_id = ${streamId} AND partition = ${options?.partition ?? defaultTag2} AND is_archived = FALSE ${SQL17.plain(fromCondition)} ${SQL17.plain(toCondition)}
4637
+ ORDER BY stream_position ASC`
4638
+ ),
4639
+ (row) => {
4640
+ const rawEvent = {
4641
+ type: row.message_type,
4642
+ data: row.message_data,
4643
+ metadata: row.message_metadata
4644
+ };
4645
+ const metadata = {
4646
+ ..."metadata" in rawEvent ? rawEvent.metadata ?? {} : {},
4647
+ messageId: row.message_id,
4648
+ streamName: streamId,
4649
+ streamPosition: BigInt(row.stream_position),
4650
+ globalPosition: BigInt(row.global_position),
4651
+ checkpoint: bigIntProcessorCheckpoint(BigInt(row.global_position))
4342
4652
  };
4343
- return (givenEvents) => {
4344
- return {
4345
- when: (events, options2) => {
4346
- const allEvents = [];
4347
- const run = async (pool) => {
4348
- let globalPosition = 0n;
4349
- const numberOfTimes = options2?.numberOfTimes ?? 1;
4350
- for (const event of [
4351
- ...givenEvents,
4352
- ...Array.from({ length: numberOfTimes }).flatMap(() => events)
4353
- ]) {
4354
- const metadata = {
4355
- checkpoint: bigIntProcessorCheckpoint(++globalPosition),
4356
- globalPosition,
4357
- streamPosition: globalPosition,
4358
- streamName: `test-${uuid6()}`,
4359
- messageId: uuid6()
4360
- };
4361
- allEvents.push({
4362
- ...event,
4363
- kind: "Event",
4364
- metadata: {
4365
- ...metadata,
4366
- ..."metadata" in event ? event.metadata ?? {} : {}
4367
- }
4368
- });
4369
- }
4370
- await initialize(pool);
4371
- await pool.withTransaction(async (transaction) => {
4372
- await handleProjections({
4373
- events: allEvents,
4374
- projections: [projection2],
4375
- ...await transactionToPostgreSQLProjectionHandlerContext(
4376
- connectionString,
4377
- pool,
4378
- transaction
4379
- )
4380
- });
4381
- });
4382
- };
4383
- return {
4384
- then: async (assert, message) => {
4385
- const pool = dumbo4(dumoOptions);
4386
- try {
4387
- await run(pool);
4388
- const succeeded = await assert({ pool, connectionString });
4389
- if (succeeded !== void 0 && succeeded === false)
4390
- assertFails(
4391
- message ?? "Projection specification didn't match the criteria"
4392
- );
4393
- } finally {
4394
- await pool.close();
4395
- }
4396
- },
4397
- thenThrows: async (...args) => {
4398
- const pool = dumbo4(dumoOptions);
4399
- try {
4400
- await run(pool);
4401
- throw new AssertionError("Handler did not fail as expected");
4402
- } catch (error) {
4403
- if (error instanceof AssertionError) throw error;
4404
- if (args.length === 0) return;
4405
- if (!isErrorConstructor(args[0])) {
4406
- assertTrue(
4407
- args[0](error),
4408
- `Error didn't match the error condition: ${error?.toString()}`
4409
- );
4410
- return;
4411
- }
4412
- assertTrue(
4413
- error instanceof args[0],
4414
- `Caught error is not an instance of the expected type: ${error?.toString()}`
4415
- );
4416
- if (args[1]) {
4417
- assertTrue(
4418
- args[1](error),
4419
- `Error didn't match the error condition: ${error?.toString()}`
4420
- );
4421
- }
4422
- } finally {
4423
- await pool.close();
4424
- }
4425
- }
4426
- };
4427
- }
4428
- };
4653
+ const event = {
4654
+ ...rawEvent,
4655
+ kind: "Event",
4656
+ metadata
4429
4657
  };
4658
+ return upcastRecordedMessage(event, options?.schema?.versioning);
4430
4659
  }
4431
- }
4432
- };
4433
- var eventInStream = (streamName, event) => {
4434
- return {
4435
- ...event,
4436
- metadata: {
4437
- ...event.metadata ?? {},
4438
- streamName: event.metadata?.streamName ?? streamName
4439
- }
4660
+ );
4661
+ return events.length > 0 ? {
4662
+ currentStreamVersion: events[events.length - 1].metadata.streamPosition,
4663
+ events,
4664
+ streamExists: true
4665
+ } : {
4666
+ currentStreamVersion: PostgreSQLEventStoreDefaultStreamVersion,
4667
+ events: [],
4668
+ streamExists: false
4440
4669
  };
4441
4670
  };
4442
- var eventsInStream = (streamName, events) => {
4443
- return events.map((e) => eventInStream(streamName, e));
4444
- };
4445
- var newEventsInStream = eventsInStream;
4446
- var assertSQLQueryResultMatches = (sql, rows) => async ({ pool: { execute } }) => {
4447
- const result = await execute.query(sql);
4448
- assertThatArray(rows).containsExactlyInAnyOrder(result.rows);
4671
+
4672
+ // src/eventStore/schema/streamExists.ts
4673
+ import { SQL as SQL18 } from "@event-driven-io/dumbo";
4674
+ var streamExists = async (execute, streamId, options) => {
4675
+ const queryResult = await execute.query(
4676
+ SQL18`SELECT EXISTS (
4677
+ SELECT 1
4678
+ from ${SQL18.identifier(streamsTable.name)}
4679
+ WHERE stream_id = ${streamId} AND partition = ${options?.partition ?? defaultTag2} AND is_archived = FALSE)
4680
+ `
4681
+ );
4682
+ return queryResult.rows[0]?.exists ?? false;
4449
4683
  };
4450
- var expectSQL = {
4451
- query: (sql) => ({
4452
- resultRows: {
4453
- toBeTheSame: (rows) => assertSQLQueryResultMatches(sql, rows)
4684
+
4685
+ // src/eventStore/schema/index.ts
4686
+ var schemaSQL = [
4687
+ streamsTableSQL,
4688
+ messagesTableSQL,
4689
+ projectionsTableSQL,
4690
+ processorsTableSQL,
4691
+ sanitizeNameSQL,
4692
+ addTablePartitions,
4693
+ addPartitionSQL,
4694
+ appendToStreamSQL,
4695
+ addDefaultPartitionSQL,
4696
+ storeSubscriptionCheckpointSQL,
4697
+ tryAcquireProcessorLockSQL,
4698
+ releaseProcessorLockSQL,
4699
+ registerProjectionSQL,
4700
+ activateProjectionSQL,
4701
+ deactivateProjectionSQL
4702
+ ];
4703
+ var schemaMigration = sqlMigration4(
4704
+ "emt:postgresql:eventstore:initial",
4705
+ schemaSQL
4706
+ );
4707
+ var eventStoreSchemaMigrations = [
4708
+ migration_0_38_7_and_older,
4709
+ migration_0_42_0_FromSubscriptionsToProcessors,
4710
+ migration_0_42_0_2_AddProcessorProjectionFunctions,
4711
+ schemaMigration
4712
+ ];
4713
+ var createEventStoreSchema = (connectionString, pool, hooks, options) => {
4714
+ return pool.withTransaction(async (tx) => {
4715
+ const context = await transactionToPostgreSQLProjectionHandlerContext(
4716
+ connectionString,
4717
+ pool,
4718
+ tx
4719
+ );
4720
+ const nestedPool = dumbo3({
4721
+ connectionString,
4722
+ connection: tx.connection,
4723
+ serialization: options?.serialization
4724
+ });
4725
+ try {
4726
+ if (hooks?.onBeforeSchemaCreated) {
4727
+ await hooks.onBeforeSchemaCreated(context);
4728
+ }
4729
+ const result = await runSQLMigrations(
4730
+ nestedPool,
4731
+ eventStoreSchemaMigrations,
4732
+ options
4733
+ );
4734
+ if (hooks?.onAfterSchemaCreated) {
4735
+ await hooks.onAfterSchemaCreated(context);
4736
+ }
4737
+ return result;
4738
+ } finally {
4739
+ await nestedPool.close();
4454
4740
  }
4455
- })
4741
+ });
4456
4742
  };
4457
4743
 
4458
- // src/eventStore/projections/postgreSQLProjection.ts
4459
- var transactionToPostgreSQLProjectionHandlerContext = async (connectionString, pool, transaction) => ({
4460
- execute: transaction.execute,
4461
- connection: {
4462
- connectionString,
4463
- client: await transaction.connection.open(),
4464
- transaction,
4465
- pool
4466
- }
4467
- });
4468
- var handleProjections = async (options) => {
4469
- const {
4470
- projections: allProjections,
4471
- events,
4472
- connection: { pool, transaction, connectionString },
4473
- partition = defaultTag2
4474
- } = options;
4475
- const eventTypes = events.map((e) => e.type);
4476
- const projections = allProjections.filter(
4477
- (p) => p.canHandle.some((type) => eventTypes.includes(type))
4744
+ // src/eventStore/schema/truncateTables.ts
4745
+ import { SQL as SQL19 } from "@event-driven-io/dumbo";
4746
+ var truncateTables = async (execute, options) => {
4747
+ await execute.command(
4748
+ SQL19`TRUNCATE TABLE
4749
+ ${SQL19.identifier(streamsTable.name)},
4750
+ ${SQL19.identifier(messagesTable.name)},
4751
+ ${SQL19.identifier(processorsTable.name)},
4752
+ ${SQL19.identifier(projectionsTable.name)}
4753
+ CASCADE${SQL19.plain(options?.resetSequences ? "; ALTER SEQUENCE emt_global_message_position RESTART WITH 1" : "")};`
4478
4754
  );
4479
- const client = await transaction.connection.open();
4480
- for (const projection2 of projections) {
4481
- if (projection2.name) {
4482
- const lockAcquired = await postgreSQLProjectionLock({
4483
- projectionName: projection2.name,
4484
- partition,
4485
- version: projection2.version ?? 1
4486
- }).tryAcquire({ execute: transaction.execute });
4487
- if (!lockAcquired) {
4488
- continue;
4489
- }
4755
+ };
4756
+
4757
+ // src/eventStore/postgreSQLEventStore.ts
4758
+ var defaultPostgreSQLOptions = {
4759
+ projections: [],
4760
+ schema: { autoMigration: "CreateOrUpdate" }
4761
+ };
4762
+ var PostgreSQLEventStoreDefaultStreamVersion = 0n;
4763
+ var getPostgreSQLEventStore = (connectionString, options = defaultPostgreSQLOptions) => {
4764
+ const poolOptions = {
4765
+ connectionString,
4766
+ ...options.connectionOptions ? options.connectionOptions : {}
4767
+ };
4768
+ const pool = "dumbo" in poolOptions ? poolOptions.dumbo : dumbo4({ ...poolOptions, serialization: options.serialization });
4769
+ let migrateSchema = void 0;
4770
+ const autoGenerateSchema = options.schema?.autoMigration === void 0 || options.schema?.autoMigration !== "None";
4771
+ const inlineProjections = (options.projections ?? []).filter(({ type }) => type === "inline").map(({ projection: projection2 }) => projection2);
4772
+ const migrate = async (migrationOptions) => {
4773
+ if (!migrateSchema) {
4774
+ migrateSchema = createEventStoreSchema(
4775
+ connectionString,
4776
+ pool,
4777
+ {
4778
+ onBeforeSchemaCreated: async (context) => {
4779
+ if (options.hooks?.onBeforeSchemaCreated) {
4780
+ await options.hooks.onBeforeSchemaCreated(context);
4781
+ }
4782
+ },
4783
+ onAfterSchemaCreated: async (context) => {
4784
+ for (const projection2 of inlineProjections) {
4785
+ if (projection2.init) {
4786
+ await projection2.init({
4787
+ version: projection2.version ?? 1,
4788
+ status: "active",
4789
+ registrationType: "inline",
4790
+ context: { ...context, migrationOptions }
4791
+ });
4792
+ }
4793
+ }
4794
+ if (options.hooks?.onAfterSchemaCreated) {
4795
+ await options.hooks.onAfterSchemaCreated(context);
4796
+ }
4797
+ }
4798
+ },
4799
+ migrationOptions
4800
+ );
4490
4801
  }
4491
- await projection2.handle(events, {
4492
- connection: {
4493
- connectionString,
4494
- pool,
4495
- client,
4496
- transaction
4497
- },
4498
- execute: transaction.execute
4499
- });
4500
- }
4501
- };
4502
- var postgreSQLProjection = (definition) => projection({
4503
- ...definition,
4504
- init: async (options) => {
4505
- await registerProjection(options.context.execute, {
4506
- // TODO: pass partition from options
4507
- partition: defaultTag2,
4508
- status: "active",
4509
- registration: {
4510
- type: "async",
4511
- // TODO: fix this
4512
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
4513
- projection: definition
4802
+ return migrateSchema;
4803
+ };
4804
+ const ensureSchemaExists = () => {
4805
+ if (!autoGenerateSchema) return Promise.resolve();
4806
+ return migrate();
4807
+ };
4808
+ const beforeCommitHook = inlineProjections.length > 0 ? async (events, { transaction }) => handleProjections({
4809
+ projections: inlineProjections,
4810
+ // TODO: Add proper handling of global data
4811
+ // Currently it's not available as append doesn't return array of global position but just the last one
4812
+ events,
4813
+ ...await transactionToPostgreSQLProjectionHandlerContext(
4814
+ connectionString,
4815
+ pool,
4816
+ transaction
4817
+ )
4818
+ }) : void 0;
4819
+ return {
4820
+ schema: {
4821
+ sql: () => SQL20.describe(
4822
+ schemaSQL,
4823
+ getFormatter(fromDatabaseDriverType(pool.driverType).databaseType)
4824
+ ),
4825
+ print: () => console.log(
4826
+ SQL20.describe(
4827
+ schemaSQL,
4828
+ getFormatter(fromDatabaseDriverType(pool.driverType).databaseType)
4829
+ )
4830
+ ),
4831
+ migrate,
4832
+ dangerous: {
4833
+ truncate: (truncateOptions) => pool.withTransaction(async (transaction) => {
4834
+ await ensureSchemaExists();
4835
+ await truncateTables(transaction.execute, truncateOptions);
4836
+ if (truncateOptions?.truncateProjections) {
4837
+ const projectionContext = await transactionToPostgreSQLProjectionHandlerContext(
4838
+ connectionString,
4839
+ pool,
4840
+ transaction
4841
+ );
4842
+ for (const projection2 of options?.projections ?? []) {
4843
+ if (projection2.projection.truncate)
4844
+ await projection2.projection.truncate(projectionContext);
4845
+ }
4846
+ }
4847
+ })
4514
4848
  }
4515
- });
4516
- if (definition.init) {
4517
- await definition.init(options);
4518
- }
4519
- }
4520
- });
4521
- var postgreSQLRawBatchSQLProjection = (options) => postgreSQLProjection({
4522
- name: options.name,
4523
- kind: options.kind ?? "emt:projections:postgresql:raw_sql:batch",
4524
- version: options.version,
4525
- canHandle: options.canHandle,
4526
- eventsOptions: options.eventsOptions,
4527
- handle: async (events, context) => {
4528
- const sqls = await options.evolve(events, context);
4529
- await context.execute.batchCommand(sqls);
4530
- },
4531
- init: async (initOptions) => {
4532
- const initSQL = options.init ? await options.init(initOptions) : void 0;
4533
- if (initSQL) {
4534
- if (Array.isArray(initSQL)) {
4535
- await initOptions.context.execute.batchCommand(initSQL);
4536
- } else {
4537
- await initOptions.context.execute.command(initSQL);
4849
+ },
4850
+ async aggregateStream(streamName, options2) {
4851
+ const { evolve, initialState, read } = options2;
4852
+ const expectedStreamVersion = read?.expectedStreamVersion;
4853
+ let state = initialState();
4854
+ const result = await this.readStream(
4855
+ streamName,
4856
+ read
4857
+ );
4858
+ const currentStreamVersion = result.currentStreamVersion;
4859
+ assertExpectedVersionMatchesCurrent(
4860
+ currentStreamVersion,
4861
+ expectedStreamVersion,
4862
+ PostgreSQLEventStoreDefaultStreamVersion
4863
+ );
4864
+ for (const event of result.events) {
4865
+ if (!event) continue;
4866
+ state = evolve(state, event);
4538
4867
  }
4539
- }
4540
- }
4541
- });
4542
- var postgreSQLRawSQLProjection = (options) => {
4543
- const { evolve, kind, ...rest } = options;
4544
- return postgreSQLRawBatchSQLProjection({
4545
- kind: kind ?? "emt:projections:postgresql:raw:_sql:single",
4546
- ...rest,
4547
- evolve: async (events, context) => {
4548
- const sqls = [];
4549
- for (const event of events) {
4550
- const pendingSqls = await evolve(event, context);
4551
- if (Array.isArray(pendingSqls)) {
4552
- sqls.push(...pendingSqls);
4553
- } else {
4554
- sqls.push(pendingSqls);
4868
+ return {
4869
+ currentStreamVersion,
4870
+ state,
4871
+ streamExists: result.streamExists
4872
+ };
4873
+ },
4874
+ readStream: async (streamName, readOptions) => {
4875
+ await ensureSchemaExists();
4876
+ return readStream(pool.execute, streamName, {
4877
+ ...readOptions,
4878
+ serialization: options.serialization ?? readOptions?.serialization
4879
+ });
4880
+ },
4881
+ appendToStream: async (streamName, events, appendOptions) => {
4882
+ await ensureSchemaExists();
4883
+ const [firstPart, ...rest] = streamName.split("-");
4884
+ const streamType = firstPart && rest.length > 0 ? firstPart : unknownTag2;
4885
+ const appendResult = await appendToStream(
4886
+ // TODO: Fix this when introducing more drivers
4887
+ pool,
4888
+ streamName,
4889
+ streamType,
4890
+ downcastRecordedMessages(events, appendOptions?.schema?.versioning),
4891
+ {
4892
+ ...appendOptions,
4893
+ beforeCommitHook
4555
4894
  }
4556
- }
4557
- return sqls;
4895
+ );
4896
+ if (!appendResult.success)
4897
+ throw new ExpectedVersionConflictError(
4898
+ -1n,
4899
+ //TODO: Return actual version in case of error
4900
+ appendOptions?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK
4901
+ );
4902
+ return {
4903
+ nextExpectedStreamVersion: appendResult.nextStreamPosition,
4904
+ lastEventGlobalPosition: appendResult.globalPositions[appendResult.globalPositions.length - 1],
4905
+ createdNewStream: appendResult.nextStreamPosition >= BigInt(events.length)
4906
+ };
4907
+ },
4908
+ streamExists: async (streamName, options2) => {
4909
+ await ensureSchemaExists();
4910
+ return streamExists(pool.execute, streamName, options2);
4911
+ },
4912
+ consumer: (options2) => postgreSQLEventStoreConsumer({
4913
+ ...options2 ?? {},
4914
+ pool,
4915
+ connectionString
4916
+ }),
4917
+ close: () => pool.close(),
4918
+ async withSession(callback) {
4919
+ return await pool.withConnection(async (connection) => {
4920
+ const storeOptions = {
4921
+ ...options,
4922
+ connectionOptions: {
4923
+ connection
4924
+ },
4925
+ schema: {
4926
+ ...options.schema ?? {},
4927
+ autoMigration: "None"
4928
+ }
4929
+ };
4930
+ const eventStore = getPostgreSQLEventStore(
4931
+ connectionString,
4932
+ storeOptions
4933
+ );
4934
+ return ensureSchemaExists().then(
4935
+ () => callback({
4936
+ eventStore,
4937
+ close: () => Promise.resolve()
4938
+ })
4939
+ );
4940
+ });
4558
4941
  }
4559
- });
4942
+ };
4560
4943
  };
4561
4944
 
4562
4945
  // src/eventStore/consumers/postgreSQLProcessor.ts
@@ -4602,7 +4985,10 @@ var postgreSQLProcessingScope = (options) => {
4602
4985
  connectionString,
4603
4986
  pool,
4604
4987
  client,
4605
- transaction
4988
+ transaction,
4989
+ messageStore: getPostgreSQLEventStore(connectionString, {
4990
+ connectionOptions: { client }
4991
+ })
4606
4992
  }
4607
4993
  });
4608
4994
  });
@@ -4616,7 +5002,8 @@ var getProcessorPool = (options) => {
4616
5002
  const processorConnectionString = "connectionString" in poolOptions ? poolOptions.connectionString ?? null : null;
4617
5003
  const processorPool = "dumbo" in poolOptions ? poolOptions.dumbo : processorConnectionString ? dumbo5({
4618
5004
  connectionString: processorConnectionString,
4619
- ...poolOptions
5005
+ ...poolOptions,
5006
+ serialization: options.serialization
4620
5007
  }) : null;
4621
5008
  return {
4622
5009
  pool: processorPool,
@@ -4704,6 +5091,53 @@ var postgreSQLProjector = (options) => {
4704
5091
  });
4705
5092
  return processor;
4706
5093
  };
5094
+ var postgreSQLWorkflowProcessor = (options) => {
5095
+ const {
5096
+ processorId = options.processorId ?? getWorkflowId({
5097
+ workflowName: options.workflow.name ?? "unknown"
5098
+ }),
5099
+ processorInstanceId = getProcessorInstanceId(processorId),
5100
+ version = defaultProcessorVersion,
5101
+ partition = defaultProcessorPartition,
5102
+ lock
5103
+ } = options;
5104
+ const { pool, connectionString, close } = getProcessorPool(options);
5105
+ const processorLock = postgreSQLProcessorLock({
5106
+ processorId,
5107
+ version,
5108
+ partition,
5109
+ processorInstanceId,
5110
+ projection: void 0,
5111
+ lockAcquisitionPolicy: lock?.acquisitionPolicy ?? DefaultPostgreSQLProcessorLockPolicy,
5112
+ lockTimeoutSeconds: lock?.timeoutSeconds
5113
+ });
5114
+ const hooks = wrapHooksWithProcessorLocks(
5115
+ {
5116
+ ...options.hooks ?? {},
5117
+ onClose: close ? async (context) => {
5118
+ if (options.hooks?.onClose)
5119
+ await options.hooks?.onClose(context);
5120
+ if (close) await close();
5121
+ } : options.hooks?.onClose
5122
+ },
5123
+ processorLock
5124
+ );
5125
+ return workflowProcessor({
5126
+ ...options,
5127
+ processorId,
5128
+ processorInstanceId,
5129
+ version,
5130
+ partition,
5131
+ hooks,
5132
+ processingScope: postgreSQLProcessingScope({
5133
+ pool,
5134
+ connectionString,
5135
+ processorId,
5136
+ partition
5137
+ }),
5138
+ checkpoints: postgreSQLCheckpointer()
5139
+ });
5140
+ };
4707
5141
  var postgreSQLReactor = (options) => {
4708
5142
  const {
4709
5143
  processorId = options.processorId,
@@ -4758,7 +5192,10 @@ var postgreSQLEventStoreConsumer = (options) => {
4758
5192
  let abortController = null;
4759
5193
  let start;
4760
5194
  let messagePuller;
4761
- const pool = options.pool ? options.pool : dumbo6({ connectionString: options.connectionString });
5195
+ const pool = options.pool ? options.pool : dumbo6({
5196
+ connectionString: options.connectionString,
5197
+ serialization: options.serialization
5198
+ });
4762
5199
  const eachBatch = async (messagesBatch) => {
4763
5200
  const activeProcessors = processors.filter((s) => s.isActive);
4764
5201
  if (activeProcessors.length === 0)
@@ -4788,7 +5225,8 @@ var postgreSQLEventStoreConsumer = (options) => {
4788
5225
  connectionString: options.connectionString,
4789
5226
  pool,
4790
5227
  client: void 0,
4791
- transaction: void 0
5228
+ transaction: void 0,
5229
+ messageStore: void 0
4792
5230
  }
4793
5231
  };
4794
5232
  const stopProcessors = () => Promise.all(processors.map((p) => p.close(processorContext)));
@@ -4798,10 +5236,10 @@ var postgreSQLEventStoreConsumer = (options) => {
4798
5236
  if (messagePuller) {
4799
5237
  abortController?.abort();
4800
5238
  await messagePuller.stop();
4801
- messagePuller = void 0;
4802
- abortController = null;
4803
5239
  }
4804
5240
  await start;
5241
+ messagePuller = void 0;
5242
+ abortController = null;
4805
5243
  await stopProcessors();
4806
5244
  };
4807
5245
  const init = async () => {
@@ -4815,7 +5253,7 @@ var postgreSQLEventStoreConsumer = (options) => {
4815
5253
  isInitialized = true;
4816
5254
  };
4817
5255
  return {
4818
- consumerId: options.consumerId ?? uuid7(),
5256
+ consumerId: options.consumerId ?? uuid9(),
4819
5257
  get isRunning() {
4820
5258
  return isRunning;
4821
5259
  },
@@ -4837,6 +5275,14 @@ var postgreSQLEventStoreConsumer = (options) => {
4837
5275
  );
4838
5276
  return processor;
4839
5277
  },
5278
+ workflowProcessor: (options2) => {
5279
+ const processor = postgreSQLWorkflowProcessor(options2);
5280
+ processors.push(
5281
+ // TODO: change that
5282
+ processor
5283
+ );
5284
+ return processor;
5285
+ },
4840
5286
  start: () => {
4841
5287
  if (isRunning) return start;
4842
5288
  if (processors.length === 0)
@@ -4986,6 +5432,7 @@ export {
4986
5432
  postgreSQLRawBatchSQLProjection,
4987
5433
  postgreSQLRawSQLProjection,
4988
5434
  postgreSQLReactor,
5435
+ postgreSQLWorkflowProcessor,
4989
5436
  processorsTable,
4990
5437
  processorsTableSQL,
4991
5438
  projectionsTable,