@event-driven-io/emmett-postgresql 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/eventStore/postgreSQLEventStore.ts
2
2
  import {
3
- dumbo as dumbo2
3
+ dumbo as dumbo3
4
4
  } from "@event-driven-io/dumbo";
5
5
 
6
6
  // ../emmett/dist/chunk-4E7QLAH5.js
@@ -436,352 +436,179 @@ var assertThatArray = (array) => {
436
436
  // src/eventStore/postgreSQLEventStore.ts
437
437
  import "pg";
438
438
 
439
- // src/eventStore/projections/index.ts
439
+ // src/eventStore/consumers/messageBatchProcessing/index.ts
440
440
  import "@event-driven-io/dumbo";
441
441
 
442
- // src/eventStore/projections/pongo/pongoProjectionSpec.ts
443
- import "@event-driven-io/dumbo";
444
- import {
445
- pongoClient
446
- } from "@event-driven-io/pongo";
447
- var withCollection = (handle, options) => {
448
- const { pool, connectionString, inDatabase, inCollection } = options;
449
- return pool.withConnection(async (connection) => {
450
- const pongo = pongoClient(connectionString, {
451
- connectionOptions: { connection }
452
- });
453
- try {
454
- const collection = pongo.db(inDatabase).collection(inCollection);
455
- return handle(collection);
456
- } finally {
457
- await pongo.close();
458
- }
459
- });
460
- };
461
- var withoutIdAndVersion = (doc) => {
462
- const { _id, _version, ...without } = doc;
463
- return without;
464
- };
465
- var assertDocumentsEqual = (actual, expected) => {
466
- if ("_id" in expected)
467
- assertEqual(
468
- expected._id,
469
- actual._id,
470
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
471
- `Document ids are not matching! Expected: ${expected._id}, Actual: ${actual._id}`
472
- );
473
- return assertDeepEqual(
474
- withoutIdAndVersion(actual),
475
- withoutIdAndVersion(expected)
476
- );
442
+ // src/eventStore/schema/readLastMessageGlobalPosition.ts
443
+ import { singleOrNull, sql } from "@event-driven-io/dumbo";
444
+
445
+ // src/eventStore/schema/typing.ts
446
+ var emmettPrefix = "emt";
447
+ var globalTag = "global";
448
+ var defaultTag = "emt:default";
449
+ var globalNames = {
450
+ module: `${emmettPrefix}:module:${globalTag}`,
451
+ tenant: `${emmettPrefix}:tenant:${globalTag}`
477
452
  };
478
- var documentExists = (document, options) => (assertOptions) => withCollection(
479
- async (collection) => {
480
- const result = await collection.findOne(
481
- "withId" in options ? { _id: options.withId } : options.matchingFilter
482
- );
483
- assertIsNotNull(result);
484
- assertDocumentsEqual(result, document);
485
- },
486
- { ...options, ...assertOptions }
487
- );
488
- var documentsAreTheSame = (documents, options) => (assertOptions) => withCollection(
489
- async (collection) => {
490
- const result = await collection.find(
491
- "withId" in options ? { _id: options.withId } : options.matchingFilter
492
- );
493
- assertEqual(
494
- documents.length,
495
- result.length,
496
- "Different Documents Count than expected"
497
- );
498
- for (let i = 0; i < documents.length; i++) {
499
- assertThatArray(result).contains(documents[i]);
500
- }
501
- },
502
- { ...options, ...assertOptions }
503
- );
504
- var documentsMatchingHaveCount = (expectedCount, options) => (assertOptions) => withCollection(
505
- async (collection) => {
506
- const result = await collection.find(
507
- "withId" in options ? { _id: options.withId } : options.matchingFilter
508
- );
509
- assertEqual(
510
- expectedCount,
511
- result.length,
512
- "Different Documents Count than expected"
513
- );
514
- },
515
- { ...options, ...assertOptions }
516
- );
517
- var documentMatchingExists = (options) => (assertOptions) => withCollection(
518
- async (collection) => {
519
- const result = await collection.find(
520
- "withId" in options ? { _id: options.withId } : options.matchingFilter
521
- );
522
- assertThatArray(result).isNotEmpty();
523
- },
524
- { ...options, ...assertOptions }
525
- );
526
- var documentDoesNotExist = (options) => (assertOptions) => withCollection(
527
- async (collection) => {
528
- const result = await collection.findOne(
529
- "withId" in options ? { _id: options.withId } : options.matchingFilter
530
- );
531
- assertIsNotNull(result);
453
+ var columns = {
454
+ partition: {
455
+ name: "partition"
532
456
  },
533
- { ...options, ...assertOptions }
534
- );
535
- var expectPongoDocuments = {
536
- fromCollection: (collectionName) => {
537
- return {
538
- withId: (id) => {
539
- return {
540
- toBeEqual: (document) => documentExists(document, {
541
- withId: id,
542
- inCollection: collectionName
543
- }),
544
- toExist: () => documentMatchingExists({
545
- withId: id,
546
- inCollection: collectionName
547
- }),
548
- notToExist: () => documentDoesNotExist({
549
- withId: id,
550
- inCollection: collectionName
551
- })
552
- };
553
- },
554
- matching: (filter2) => {
555
- return {
556
- toBeTheSame: (documents) => documentsAreTheSame(documents, {
557
- matchingFilter: filter2,
558
- inCollection: collectionName
559
- }),
560
- toHaveCount: (expectedCount) => documentsMatchingHaveCount(expectedCount, {
561
- matchingFilter: filter2,
562
- inCollection: collectionName
563
- }),
564
- toExist: () => documentMatchingExists({
565
- matchingFilter: filter2,
566
- inCollection: collectionName
567
- }),
568
- notToExist: () => documentDoesNotExist({
569
- matchingFilter: filter2,
570
- inCollection: collectionName
571
- })
572
- };
573
- }
574
- };
457
+ isArchived: { name: "is_archived" }
458
+ };
459
+ var streamsTable = {
460
+ name: `${emmettPrefix}_streams`,
461
+ columns: {
462
+ partition: columns.partition,
463
+ isArchived: columns.isArchived
575
464
  }
576
465
  };
577
-
578
- // src/eventStore/projections/pongo/projections.ts
579
- import {
580
- pongoClient as pongoClient2
581
- } from "@event-driven-io/pongo";
582
- var pongoProjection = ({
583
- handle,
584
- canHandle
585
- }) => postgreSQLProjection({
586
- canHandle,
587
- handle: async (events, context) => {
588
- const { connectionString, client } = context;
589
- const pongo = pongoClient2(connectionString, {
590
- connectionOptions: { client }
591
- });
592
- await handle(events, {
593
- ...context,
594
- pongo
595
- });
466
+ var eventsTable = {
467
+ name: `${emmettPrefix}_events`,
468
+ columns: {
469
+ partition: columns.partition,
470
+ isArchived: columns.isArchived
596
471
  }
597
- });
598
- var pongoMultiStreamProjection = (options) => {
599
- const { collectionName, getDocumentId, canHandle } = options;
600
- return pongoProjection({
601
- handle: async (events, { pongo }) => {
602
- const collection = pongo.db().collection(collectionName);
603
- for (const event of events) {
604
- await collection.handle(getDocumentId(event), async (document) => {
605
- return "initialState" in options ? await options.evolve(
606
- document ?? options.initialState(),
607
- event
608
- ) : await options.evolve(
609
- document,
610
- event
611
- );
612
- });
613
- }
614
- },
615
- canHandle
616
- });
617
472
  };
618
- var pongoSingleStreamProjection = (options) => {
619
- return pongoMultiStreamProjection({
620
- ...options,
621
- getDocumentId: options.getDocumentId ?? ((event) => event.metadata.streamName)
622
- });
473
+ var subscriptionsTable = {
474
+ name: `${emmettPrefix}_subscriptions`
623
475
  };
624
476
 
625
- // src/eventStore/projections/postgresProjectionSpec.ts
626
- import {
627
- dumbo
628
- } from "@event-driven-io/dumbo";
629
- import { v4 as uuid4 } from "uuid";
630
- var PostgreSQLProjectionSpec = {
631
- for: (options) => {
632
- {
633
- const { projection: projection2, ...dumoOptions } = options;
634
- const { connectionString } = dumoOptions;
635
- return (givenEvents) => {
636
- return {
637
- when: (events, options2) => {
638
- const allEvents = [];
639
- const run = async (pool) => {
640
- let globalPosition = 0n;
641
- const numberOfTimes = options2?.numberOfTimes ?? 1;
642
- for (const event of [
643
- ...givenEvents,
644
- ...Array.from({ length: numberOfTimes }).flatMap(() => events)
645
- ]) {
646
- const metadata = {
647
- globalPosition: ++globalPosition,
648
- streamPosition: globalPosition,
649
- streamName: `test-${uuid4()}`,
650
- eventId: uuid4()
651
- };
652
- allEvents.push({
653
- ...event,
654
- metadata: {
655
- ...metadata,
656
- ..."metadata" in event ? event.metadata ?? {} : {}
657
- }
658
- });
659
- }
660
- await pool.withTransaction(
661
- (transaction) => handleProjections({
662
- events: allEvents,
663
- projections: [projection2],
664
- connection: {
665
- connectionString,
666
- transaction
667
- }
668
- })
669
- );
670
- };
671
- return {
672
- then: async (assert, message) => {
673
- const pool = dumbo(dumoOptions);
674
- try {
675
- await run(pool);
676
- const succeeded = await assert({ pool, connectionString });
677
- if (succeeded !== void 0 && succeeded === false)
678
- assertFails(
679
- message ?? "Projection specification didn't match the criteria"
680
- );
681
- } finally {
682
- await pool.close();
683
- }
684
- },
685
- thenThrows: async (...args) => {
686
- const pool = dumbo(dumoOptions);
687
- try {
688
- await run(pool);
689
- throw new AssertionError("Handler did not fail as expected");
690
- } catch (error) {
691
- if (error instanceof AssertionError) throw error;
692
- if (args.length === 0) return;
693
- if (!isErrorConstructor(args[0])) {
694
- assertTrue(
695
- args[0](error),
696
- `Error didn't match the error condition: ${error?.toString()}`
697
- );
698
- return;
699
- }
700
- assertTrue(
701
- error instanceof args[0],
702
- `Caught error is not an instance of the expected type: ${error?.toString()}`
703
- );
704
- if (args[1]) {
705
- assertTrue(
706
- args[1](error),
707
- `Error didn't match the error condition: ${error?.toString()}`
708
- );
709
- }
710
- } finally {
711
- await pool.close();
712
- }
713
- }
714
- };
715
- }
716
- };
477
+ // src/eventStore/schema/readLastMessageGlobalPosition.ts
478
+ var readLastMessageGlobalPosition = async (execute, options) => {
479
+ const result = await singleOrNull(
480
+ execute.query(
481
+ sql(
482
+ `SELECT global_position
483
+ FROM ${eventsTable.name}
484
+ WHERE partition = %L AND is_archived = FALSE AND transaction_id < pg_snapshot_xmin(pg_current_snapshot())
485
+ ORDER BY transaction_id, global_position
486
+ LIMIT 1`,
487
+ options?.partition ?? defaultTag
488
+ )
489
+ )
490
+ );
491
+ return {
492
+ currentGlobalPosition: result !== null ? BigInt(result.global_position) : null
493
+ };
494
+ };
495
+
496
+ // src/eventStore/schema/readMessagesBatch.ts
497
+ import { mapRows, sql as sql2 } from "@event-driven-io/dumbo";
498
+ var readMessagesBatch = async (execute, options) => {
499
+ const from = "from" in options ? options.from : "after" in options ? options.after + 1n : 0n;
500
+ const batchSize = options && "batchSize" in options ? options.batchSize : options.to - options.from;
501
+ const fromCondition = from !== -0n ? `AND global_position >= ${from}` : "";
502
+ const toCondition = "to" in options ? `AND global_position <= ${options.to}` : "";
503
+ const limitCondition = "batchSize" in options ? `LIMIT ${options.batchSize}` : "";
504
+ const events = await mapRows(
505
+ execute.query(
506
+ sql2(
507
+ `SELECT stream_id, stream_position, global_position, event_data, event_metadata, event_schema_version, event_type, event_id
508
+ FROM ${eventsTable.name}
509
+ WHERE partition = %L AND is_archived = FALSE AND transaction_id < pg_snapshot_xmin(pg_current_snapshot()) ${fromCondition} ${toCondition}
510
+ ORDER BY transaction_id, global_position
511
+ ${limitCondition}`,
512
+ options?.partition ?? defaultTag
513
+ )
514
+ ),
515
+ (row) => {
516
+ const rawEvent = {
517
+ type: row.event_type,
518
+ data: row.event_data,
519
+ metadata: row.event_metadata
520
+ };
521
+ const metadata = {
522
+ ..."metadata" in rawEvent ? rawEvent.metadata ?? {} : {},
523
+ eventId: row.event_id,
524
+ streamName: row.stream_id,
525
+ streamPosition: BigInt(row.stream_position),
526
+ globalPosition: BigInt(row.global_position)
527
+ };
528
+ return {
529
+ ...rawEvent,
530
+ metadata
717
531
  };
718
532
  }
719
- }
533
+ );
534
+ return events.length > 0 ? {
535
+ currentGlobalPosition: events[events.length - 1].metadata.globalPosition,
536
+ messages: events,
537
+ areEventsLeft: events.length === batchSize
538
+ } : {
539
+ currentGlobalPosition: "from" in options ? options.from : "after" in options ? options.after : 0n,
540
+ messages: [],
541
+ areEventsLeft: false
542
+ };
720
543
  };
721
- var eventInStream = (streamName, event) => {
544
+
545
+ // src/eventStore/consumers/messageBatchProcessing/index.ts
546
+ var DefaultPostgreSQLEventStoreProcessorBatchSize = 100;
547
+ var DefaultPostgreSQLEventStoreProcessorPullingFrequencyInMs = 50;
548
+ var postgreSQLEventStoreMessageBatchPuller = ({
549
+ executor,
550
+ batchSize,
551
+ eachBatch,
552
+ pullingFrequencyInMs
553
+ }) => {
554
+ let isRunning = false;
555
+ let start;
556
+ const pullMessages = async (options) => {
557
+ const after = options.startFrom === "BEGINNING" ? 0n : options.startFrom === "END" ? (await readLastMessageGlobalPosition(executor)).currentGlobalPosition ?? 0n : options.startFrom.globalPosition;
558
+ const readMessagesOptions = {
559
+ after,
560
+ batchSize
561
+ };
562
+ let waitTime = 100;
563
+ do {
564
+ const { messages, currentGlobalPosition, areEventsLeft } = await readMessagesBatch(executor, readMessagesOptions);
565
+ if (messages.length > 0) {
566
+ const result = await eachBatch({ messages });
567
+ if (result && result.type === "STOP") {
568
+ isRunning = false;
569
+ break;
570
+ }
571
+ }
572
+ readMessagesOptions.after = currentGlobalPosition;
573
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
574
+ if (!areEventsLeft) {
575
+ waitTime = Math.min(waitTime * 2, 1e3);
576
+ } else {
577
+ waitTime = pullingFrequencyInMs;
578
+ }
579
+ } while (isRunning);
580
+ };
722
581
  return {
723
- ...event,
724
- metadata: {
725
- ...event.metadata ?? {},
726
- streamName: event.metadata?.streamName ?? streamName
582
+ get isRunning() {
583
+ return isRunning;
584
+ },
585
+ start: (options) => {
586
+ if (isRunning) return start;
587
+ start = (async () => {
588
+ isRunning = true;
589
+ return pullMessages(options);
590
+ })();
591
+ return start;
592
+ },
593
+ stop: async () => {
594
+ if (!isRunning) return;
595
+ isRunning = false;
596
+ await start;
727
597
  }
728
598
  };
729
599
  };
730
- var eventsInStream = (streamName, events) => {
731
- return events.map((e) => eventInStream(streamName, e));
732
- };
733
- var newEventsInStream = eventsInStream;
734
- var assertSQLQueryResultMatches = (sql7, rows) => async ({ pool: { execute } }) => {
735
- const result = await execute.query(sql7);
736
- assertThatArray(rows).containsExactlyInAnyOrder(result.rows);
737
- };
738
- var expectSQL = {
739
- query: (sql7) => ({
740
- resultRows: {
741
- toBeTheSame: (rows) => assertSQLQueryResultMatches(sql7, rows)
742
- }
743
- })
600
+ var zipPostgreSQLEventStoreMessageBatchPullerStartFrom = (options) => {
601
+ if (options.length === 0 || options.some((o) => o === void 0 || o === "BEGINNING"))
602
+ return "BEGINNING";
603
+ if (options.every((o) => o === "END")) return "END";
604
+ return options.filter((o) => o !== void 0 && o !== "BEGINNING" && o !== "END").sort((a, b) => a > b ? 1 : -1)[0];
744
605
  };
745
606
 
746
- // src/eventStore/projections/index.ts
747
- var handleProjections = async (options) => {
748
- const {
749
- projections: allProjections,
750
- events,
751
- connection: { transaction, connectionString }
752
- } = options;
753
- const eventTypes = events.map((e) => e.type);
754
- const projections = allProjections.filter(
755
- (p) => p.canHandle.some((type) => eventTypes.includes(type))
756
- );
757
- const client = await transaction.connection.open();
758
- for (const projection2 of projections) {
759
- await projection2.handle(events, {
760
- connectionString,
761
- client,
762
- transaction,
763
- execute: transaction.execute
764
- });
765
- }
766
- };
767
- var postgreSQLProjection = (definition) => projection(definition);
768
- var postgreSQLRawBatchSQLProjection = (handle, ...canHandle) => postgreSQLProjection({
769
- canHandle,
770
- handle: async (events, context) => {
771
- const sqls = await handle(events, context);
772
- await context.execute.batchCommand(sqls);
773
- }
774
- });
775
- var postgreSQLRawSQLProjection = (handle, ...canHandle) => postgreSQLRawBatchSQLProjection(
776
- async (events, context) => {
777
- const sqls = [];
778
- for (const event of events) {
779
- sqls.push(await handle(event, context));
780
- }
781
- return sqls;
782
- },
783
- ...canHandle
784
- );
607
+ // src/eventStore/consumers/postgreSQLEventStoreConsumer.ts
608
+ import { dumbo } from "@event-driven-io/dumbo";
609
+
610
+ // src/eventStore/consumers/postgreSQLProcessor.ts
611
+ import "@event-driven-io/dumbo";
785
612
 
786
613
  // src/eventStore/schema/index.ts
787
614
  import "@event-driven-io/dumbo";
@@ -790,43 +617,9 @@ import "@event-driven-io/dumbo";
790
617
  import {
791
618
  rawSql,
792
619
  single,
793
- sql
620
+ sql as sql3
794
621
  } from "@event-driven-io/dumbo";
795
- import { v4 as uuid5 } from "uuid";
796
-
797
- // src/eventStore/schema/typing.ts
798
- var emmettPrefix = "emt";
799
- var globalTag = "global";
800
- var defaultTag = "emt:default";
801
- var globalNames = {
802
- module: `${emmettPrefix}:module:${globalTag}`,
803
- tenant: `${emmettPrefix}:tenant:${globalTag}`
804
- };
805
- var columns = {
806
- partition: {
807
- name: "partition"
808
- },
809
- isArchived: { name: "is_archived" }
810
- };
811
- var streamsTable = {
812
- name: `${emmettPrefix}_streams`,
813
- columns: {
814
- partition: columns.partition,
815
- isArchived: columns.isArchived
816
- }
817
- };
818
- var eventsTable = {
819
- name: `${emmettPrefix}_events`,
820
- columns: {
821
- partition: columns.partition,
822
- isArchived: columns.isArchived
823
- }
824
- };
825
- var subscriptionsTable = {
826
- name: `${emmettPrefix}_subscriptions`
827
- };
828
-
829
- // src/eventStore/schema/appendToStream.ts
622
+ import { v4 as uuid4 } from "uuid";
830
623
  var appendEventsSQL = rawSql(
831
624
  `CREATE OR REPLACE FUNCTION emt_append_event(
832
625
  v_event_ids text[],
@@ -925,7 +718,7 @@ var appendToStream = (pool, streamName, streamType, events, options) => pool.wit
925
718
  ...e,
926
719
  metadata: {
927
720
  streamName,
928
- eventId: uuid5(),
721
+ eventId: uuid4(),
929
722
  streamPosition: BigInt(i),
930
723
  ..."metadata" in e ? e.metadata ?? {} : {}
931
724
  }
@@ -976,7 +769,7 @@ var toExpectedVersion = (expected) => {
976
769
  var isOptimisticConcurrencyError = (error) => error instanceof Error && "code" in error && error.code === "23505";
977
770
  var appendEventsRaw = (execute, streamId, streamType, events, options) => single(
978
771
  execute.command(
979
- sql(
772
+ sql3(
980
773
  `SELECT * FROM emt_append_event(
981
774
  ARRAY[%s]::text[],
982
775
  ARRAY[%s]::jsonb[],
@@ -988,11 +781,11 @@ var appendEventsRaw = (execute, streamId, streamType, events, options) => single
988
781
  %s::bigint,
989
782
  %L::text
990
783
  )`,
991
- events.map((e) => sql("%L", e.metadata.eventId)).join(","),
992
- events.map((e) => sql("%L", JSONParser.stringify(e.data))).join(","),
993
- events.map((e) => sql("%L", JSONParser.stringify(e.metadata ?? {}))).join(","),
784
+ events.map((e) => sql3("%L", e.metadata.eventId)).join(","),
785
+ events.map((e) => sql3("%L", JSONParser.stringify(e.data))).join(","),
786
+ events.map((e) => sql3("%L", JSONParser.stringify(e.metadata ?? {}))).join(","),
994
787
  events.map(() => `'1'`).join(","),
995
- events.map((e) => sql("%L", e.type)).join(","),
788
+ events.map((e) => sql3("%L", e.type)).join(","),
996
789
  streamId,
997
790
  streamType,
998
791
  options?.expectedStreamVersion ?? "NULL",
@@ -1001,9 +794,9 @@ var appendEventsRaw = (execute, streamId, streamType, events, options) => single
1001
794
  )
1002
795
  );
1003
796
 
1004
- // src/eventStore/schema/storeSubscriptionCheckpoint.ts
1005
- import { single as single2, sql as sql2 } from "@event-driven-io/dumbo";
1006
- var storeSubscriptionCheckpointSQL = sql2(`
797
+ // src/eventStore/schema/storeProcessorCheckpoint.ts
798
+ import { single as single2, sql as sql4 } from "@event-driven-io/dumbo";
799
+ var storeSubscriptionCheckpointSQL = sql4(`
1007
800
  CREATE OR REPLACE FUNCTION store_subscription_checkpoint(
1008
801
  p_subscription_id VARCHAR(100),
1009
802
  p_version BIGINT,
@@ -1063,13 +856,13 @@ BEGIN
1063
856
  END;
1064
857
  $$ LANGUAGE plpgsql;
1065
858
  `);
1066
- async function storeSubscriptionCheckpoint(execute, options) {
859
+ async function storeProcessorCheckpoint(execute, options) {
1067
860
  try {
1068
861
  const { result } = await single2(
1069
862
  execute.command(
1070
- sql2(
863
+ sql4(
1071
864
  `SELECT store_subscription_checkpoint(%L, %s, %L, %L, pg_current_xact_id(), %L) as result;`,
1072
- options.subscriptionId,
865
+ options.processorId,
1073
866
  options.version ?? 1,
1074
867
  options.newPosition,
1075
868
  options.lastProcessedPosition,
@@ -1382,75 +1175,6 @@ var addDefaultPartition = rawSql2(
1382
1175
  `SELECT emt_add_partition('${defaultTag}');`
1383
1176
  );
1384
1177
 
1385
- // src/eventStore/schema/readLastMessageGlobalPosition.ts
1386
- import { singleOrNull, sql as sql3 } from "@event-driven-io/dumbo";
1387
- var readLastMessageGlobalPosition = async (execute, options) => {
1388
- const result = await singleOrNull(
1389
- execute.query(
1390
- sql3(
1391
- `SELECT global_position
1392
- FROM ${eventsTable.name}
1393
- WHERE partition = %L AND is_archived = FALSE AND transaction_id < pg_snapshot_xmin(pg_current_snapshot())
1394
- ORDER BY transaction_id, global_position
1395
- LIMIT 1`,
1396
- options?.partition ?? defaultTag
1397
- )
1398
- )
1399
- );
1400
- return {
1401
- currentGlobalPosition: result !== null ? BigInt(result.global_position) : null
1402
- };
1403
- };
1404
-
1405
- // src/eventStore/schema/readMessagesBatch.ts
1406
- import { mapRows, sql as sql4 } from "@event-driven-io/dumbo";
1407
- var readMessagesBatch = async (execute, options) => {
1408
- const from = "from" in options ? options.from : "after" in options ? options.after + 1n : 0n;
1409
- const batchSize = options && "batchSize" in options ? options.batchSize : options.to - options.from;
1410
- const fromCondition = from !== -0n ? `AND global_position >= ${from}` : "";
1411
- const toCondition = "to" in options ? `AND global_position <= ${options.to}` : "";
1412
- const limitCondition = "batchSize" in options ? `LIMIT ${options.batchSize}` : "";
1413
- const events = await mapRows(
1414
- execute.query(
1415
- sql4(
1416
- `SELECT stream_id, stream_position, global_position, event_data, event_metadata, event_schema_version, event_type, event_id
1417
- FROM ${eventsTable.name}
1418
- WHERE partition = %L AND is_archived = FALSE AND transaction_id < pg_snapshot_xmin(pg_current_snapshot()) ${fromCondition} ${toCondition}
1419
- ORDER BY transaction_id, global_position
1420
- ${limitCondition}`,
1421
- options?.partition ?? defaultTag
1422
- )
1423
- ),
1424
- (row) => {
1425
- const rawEvent = {
1426
- type: row.event_type,
1427
- data: row.event_data,
1428
- metadata: row.event_metadata
1429
- };
1430
- const metadata = {
1431
- ..."metadata" in rawEvent ? rawEvent.metadata ?? {} : {},
1432
- eventId: row.event_id,
1433
- streamName: row.stream_id,
1434
- streamPosition: BigInt(row.stream_position),
1435
- globalPosition: BigInt(row.global_position)
1436
- };
1437
- return {
1438
- ...rawEvent,
1439
- metadata
1440
- };
1441
- }
1442
- );
1443
- return events.length > 0 ? {
1444
- currentGlobalPosition: events[events.length - 1].metadata.globalPosition,
1445
- messages: events,
1446
- areEventsLeft: events.length === batchSize
1447
- } : {
1448
- currentGlobalPosition: "from" in options ? options.from : "after" in options ? options.after : 0n,
1449
- messages: [],
1450
- areEventsLeft: false
1451
- };
1452
- };
1453
-
1454
1178
  // src/eventStore/schema/readStream.ts
1455
1179
  import { mapRows as mapRows2, sql as sql5 } from "@event-driven-io/dumbo";
1456
1180
  var readStream = async (execute, streamId, options) => {
@@ -1499,9 +1223,9 @@ var readStream = async (execute, streamId, options) => {
1499
1223
  };
1500
1224
  };
1501
1225
 
1502
- // src/eventStore/schema/readSubscriptionCheckpoint.ts
1226
+ // src/eventStore/schema/readProcessorCheckpoint.ts
1503
1227
  import { singleOrNull as singleOrNull2, sql as sql6 } from "@event-driven-io/dumbo";
1504
- var readSubscriptionCheckpoint = async (execute, options) => {
1228
+ var readProcessorCheckpoint = async (execute, options) => {
1505
1229
  const result = await singleOrNull2(
1506
1230
  execute.query(
1507
1231
  sql6(
@@ -1510,7 +1234,7 @@ var readSubscriptionCheckpoint = async (execute, options) => {
1510
1234
  WHERE partition = %L AND subscription_id = %L
1511
1235
  LIMIT 1`,
1512
1236
  options?.partition ?? defaultTag,
1513
- options.subscriptionId
1237
+ options.processorId
1514
1238
  )
1515
1239
  )
1516
1240
  );
@@ -1539,188 +1263,8 @@ var createEventStoreSchema = async (pool) => {
1539
1263
  await pool.withTransaction(({ execute }) => execute.batchCommand(schemaSQL));
1540
1264
  };
1541
1265
 
1542
- // src/eventStore/postgreSQLEventStore.ts
1543
- var defaultPostgreSQLOptions = {
1544
- projections: [],
1545
- schema: { autoMigration: "CreateOrUpdate" }
1546
- };
1547
- var PostgreSQLEventStoreDefaultStreamVersion = 0n;
1548
- var getPostgreSQLEventStore = (connectionString, options = defaultPostgreSQLOptions) => {
1549
- const poolOptions = {
1550
- connectionString,
1551
- ...options.connectionOptions ? options.connectionOptions : {}
1552
- };
1553
- const pool = "dumbo" in poolOptions ? poolOptions.dumbo : dumbo2(poolOptions);
1554
- let migrateSchema;
1555
- const autoGenerateSchema = options.schema?.autoMigration === void 0 || options.schema?.autoMigration !== "None";
1556
- const ensureSchemaExists = () => {
1557
- if (!autoGenerateSchema) return Promise.resolve();
1558
- if (!migrateSchema) {
1559
- migrateSchema = createEventStoreSchema(pool);
1560
- }
1561
- return migrateSchema;
1562
- };
1563
- const inlineProjections = (options.projections ?? []).filter(({ type }) => type === "inline").map(({ projection: projection2 }) => projection2);
1564
- const preCommitHook = inlineProjections.length > 0 ? (events, { transaction }) => handleProjections({
1565
- projections: inlineProjections,
1566
- connection: {
1567
- connectionString,
1568
- transaction
1569
- },
1570
- // TODO: Add proper handling of global data
1571
- // Currently it's not available as append doesn't return array of global position but just the last one
1572
- events
1573
- }) : void 0;
1574
- return {
1575
- schema: {
1576
- sql: () => schemaSQL.join(""),
1577
- print: () => console.log(schemaSQL.join("")),
1578
- migrate: async () => {
1579
- await (migrateSchema = createEventStoreSchema(pool));
1580
- }
1581
- },
1582
- async aggregateStream(streamName, options2) {
1583
- const { evolve, initialState, read } = options2;
1584
- const expectedStreamVersion = read?.expectedStreamVersion;
1585
- let state = initialState();
1586
- const result = await this.readStream(streamName, options2.read);
1587
- const currentStreamVersion = result.currentStreamVersion;
1588
- assertExpectedVersionMatchesCurrent(
1589
- currentStreamVersion,
1590
- expectedStreamVersion,
1591
- PostgreSQLEventStoreDefaultStreamVersion
1592
- );
1593
- for (const event of result.events) {
1594
- if (!event) continue;
1595
- state = evolve(state, event);
1596
- }
1597
- return {
1598
- currentStreamVersion,
1599
- state,
1600
- streamExists: result.streamExists
1601
- };
1602
- },
1603
- readStream: async (streamName, options2) => {
1604
- await ensureSchemaExists();
1605
- return readStream(pool.execute, streamName, options2);
1606
- },
1607
- appendToStream: async (streamName, events, options2) => {
1608
- await ensureSchemaExists();
1609
- const [firstPart, ...rest] = streamName.split("-");
1610
- const streamType = firstPart && rest.length > 0 ? firstPart : "emt:unknown";
1611
- const appendResult = await appendToStream(
1612
- pool,
1613
- streamName,
1614
- streamType,
1615
- events,
1616
- {
1617
- ...options2,
1618
- preCommitHook
1619
- }
1620
- );
1621
- if (!appendResult.success)
1622
- throw new ExpectedVersionConflictError(
1623
- -1n,
1624
- //TODO: Return actual version in case of error
1625
- options2?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK
1626
- );
1627
- return {
1628
- nextExpectedStreamVersion: appendResult.nextStreamPosition,
1629
- lastEventGlobalPosition: appendResult.lastGlobalPosition,
1630
- createdNewStream: appendResult.nextStreamPosition >= BigInt(events.length)
1631
- };
1632
- },
1633
- close: () => pool.close(),
1634
- async withSession(callback) {
1635
- return await pool.withConnection(async (connection) => {
1636
- const storeOptions = {
1637
- ...options,
1638
- connectionOptions: {
1639
- connection
1640
- }
1641
- };
1642
- const eventStore = getPostgreSQLEventStore(
1643
- connectionString,
1644
- storeOptions
1645
- );
1646
- return callback({
1647
- eventStore,
1648
- close: () => Promise.resolve()
1649
- });
1650
- });
1651
- }
1652
- };
1653
- };
1654
-
1655
- // src/eventStore/subscriptions/messageBatchProcessing/index.ts
1656
- import "@event-driven-io/dumbo";
1657
- var DefaultPostgreSQLEventStoreSubscriptionBatchSize = 100;
1658
- var DefaultPostgreSQLEventStoreSubscriptionPullingFrequencyInMs = 50;
1659
- var postgreSQLEventStoreMessageBatchPuller = ({
1660
- executor,
1661
- batchSize,
1662
- eachBatch,
1663
- pullingFrequencyInMs
1664
- }) => {
1665
- let isRunning = false;
1666
- let start;
1667
- const pullMessages = async (options) => {
1668
- const after = options.startFrom === "BEGINNING" ? 0n : options.startFrom === "END" ? (await readLastMessageGlobalPosition(executor)).currentGlobalPosition ?? 0n : options.startFrom.globalPosition;
1669
- const readMessagesOptions = {
1670
- after,
1671
- batchSize
1672
- };
1673
- let waitTime = 100;
1674
- do {
1675
- const { messages, currentGlobalPosition, areEventsLeft } = await readMessagesBatch(executor, readMessagesOptions);
1676
- if (messages.length > 0) {
1677
- const result = await eachBatch({ messages });
1678
- if (result && result.type === "STOP") {
1679
- isRunning = false;
1680
- break;
1681
- }
1682
- }
1683
- readMessagesOptions.after = currentGlobalPosition;
1684
- await new Promise((resolve) => setTimeout(resolve, waitTime));
1685
- if (!areEventsLeft) {
1686
- waitTime = Math.min(waitTime * 2, 1e3);
1687
- } else {
1688
- waitTime = pullingFrequencyInMs;
1689
- }
1690
- } while (isRunning);
1691
- };
1692
- return {
1693
- get isRunning() {
1694
- return isRunning;
1695
- },
1696
- start: (options) => {
1697
- if (isRunning) return start;
1698
- start = (async () => {
1699
- isRunning = true;
1700
- return pullMessages(options);
1701
- })();
1702
- return start;
1703
- },
1704
- stop: async () => {
1705
- if (!isRunning) return;
1706
- isRunning = false;
1707
- await start;
1708
- }
1709
- };
1710
- };
1711
- var zipPostgreSQLEventStoreMessageBatchPullerStartFrom = (options) => {
1712
- if (options.length === 0 || options.some((o) => o === void 0 || o === "BEGINNING"))
1713
- return "BEGINNING";
1714
- if (options.every((o) => o === "END")) return "END";
1715
- return options.filter((o) => o !== void 0 && o !== "BEGINNING" && o !== "END").sort((a, b) => a > b ? 1 : -1)[0];
1716
- };
1717
-
1718
- // src/eventStore/subscriptions/postgreSQLEventStoreConsumer.ts
1719
- import { dumbo as dumbo3 } from "@event-driven-io/dumbo";
1720
-
1721
- // src/eventStore/subscriptions/postgreSQLEventStoreSubscription.ts
1722
- import "@event-driven-io/dumbo";
1723
- var PostgreSQLEventStoreSubscription = {
1266
+ // src/eventStore/consumers/postgreSQLProcessor.ts
1267
+ var PostgreSQLProcessor = {
1724
1268
  result: {
1725
1269
  skip: (options) => ({
1726
1270
  type: "SKIP",
@@ -1732,21 +1276,18 @@ var PostgreSQLEventStoreSubscription = {
1732
1276
  })
1733
1277
  }
1734
1278
  };
1735
- var postgreSQLEventStoreSubscription = (options) => {
1279
+ var postgreSQLProcessor = (options) => {
1736
1280
  const { eachMessage } = options;
1737
1281
  let isActive = true;
1738
1282
  return {
1739
- id: options.subscriptionId,
1283
+ id: options.processorId,
1740
1284
  start: async (execute) => {
1741
1285
  isActive = true;
1742
1286
  if (options.startFrom !== "CURRENT") return options.startFrom;
1743
- const { lastProcessedPosition } = await readSubscriptionCheckpoint(
1744
- execute,
1745
- {
1746
- subscriptionId: options.subscriptionId,
1747
- partition: options.partition
1748
- }
1749
- );
1287
+ const { lastProcessedPosition } = await readProcessorCheckpoint(execute, {
1288
+ processorId: options.processorId,
1289
+ partition: options.partition
1290
+ });
1750
1291
  if (lastProcessedPosition === null) return "BEGINNING";
1751
1292
  return { globalPosition: lastProcessedPosition };
1752
1293
  },
@@ -1761,8 +1302,8 @@ var postgreSQLEventStoreSubscription = (options) => {
1761
1302
  for (const message of messages) {
1762
1303
  const typedMessage = message;
1763
1304
  const messageProcessingResult = await eachMessage(typedMessage);
1764
- await storeSubscriptionCheckpoint(tx.execute, {
1765
- subscriptionId: options.subscriptionId,
1305
+ await storeProcessorCheckpoint(tx.execute, {
1306
+ processorId: options.processorId,
1766
1307
  version: options.version,
1767
1308
  lastProcessedPosition,
1768
1309
  newPosition: typedMessage.metadata.globalPosition,
@@ -1788,23 +1329,23 @@ var postgreSQLEventStoreSubscription = (options) => {
1788
1329
  };
1789
1330
  };
1790
1331
 
1791
- // src/eventStore/subscriptions/postgreSQLEventStoreConsumer.ts
1332
+ // src/eventStore/consumers/postgreSQLEventStoreConsumer.ts
1792
1333
  var postgreSQLEventStoreConsumer = (options) => {
1793
1334
  let isRunning = false;
1794
- const { connectionString, pooling } = options;
1795
- const subscriptions = options.subscriptions ?? [];
1335
+ const { pulling } = options;
1336
+ const processors = options.processors ?? [];
1796
1337
  let start;
1797
- let currentMessagePooler;
1798
- const pool = dumbo3({ connectionString });
1338
+ let currentMessagePuller;
1339
+ const pool = "pool" in options ? options.pool : dumbo({ connectionString: options.connectionString });
1799
1340
  const eachBatch = async (messagesBatch) => {
1800
- const activeSubscriptions = subscriptions.filter((s) => s.isActive);
1801
- if (activeSubscriptions.length === 0)
1341
+ const activeProcessors = processors.filter((s) => s.isActive);
1342
+ if (activeProcessors.length === 0)
1802
1343
  return {
1803
1344
  type: "STOP",
1804
- reason: "No active subscriptions"
1345
+ reason: "No active processors"
1805
1346
  };
1806
1347
  const result = await Promise.allSettled(
1807
- activeSubscriptions.map((s) => {
1348
+ activeProcessors.map((s) => {
1808
1349
  return s.handle(messagesBatch, { pool });
1809
1350
  })
1810
1351
  );
@@ -1814,44 +1355,43 @@ var postgreSQLEventStoreConsumer = (options) => {
1814
1355
  type: "STOP"
1815
1356
  };
1816
1357
  };
1817
- const messagePooler = currentMessagePooler = postgreSQLEventStoreMessageBatchPuller({
1358
+ const messagePooler = currentMessagePuller = postgreSQLEventStoreMessageBatchPuller({
1818
1359
  executor: pool.execute,
1819
1360
  eachBatch,
1820
- batchSize: pooling?.batchSize ?? DefaultPostgreSQLEventStoreSubscriptionBatchSize,
1821
- pullingFrequencyInMs: pooling?.pullingFrequencyInMs ?? DefaultPostgreSQLEventStoreSubscriptionPullingFrequencyInMs
1361
+ batchSize: pulling?.batchSize ?? DefaultPostgreSQLEventStoreProcessorBatchSize,
1362
+ pullingFrequencyInMs: pulling?.pullingFrequencyInMs ?? DefaultPostgreSQLEventStoreProcessorPullingFrequencyInMs
1822
1363
  });
1823
1364
  const stop = async () => {
1824
1365
  if (!isRunning) return;
1825
1366
  isRunning = false;
1826
- if (currentMessagePooler) {
1827
- await currentMessagePooler.stop();
1828
- currentMessagePooler = void 0;
1367
+ if (currentMessagePuller) {
1368
+ await currentMessagePuller.stop();
1369
+ currentMessagePuller = void 0;
1829
1370
  }
1830
1371
  await start;
1831
1372
  };
1832
1373
  return {
1833
- connectionString,
1834
- subscriptions,
1374
+ processors,
1835
1375
  get isRunning() {
1836
1376
  return isRunning;
1837
1377
  },
1838
- subscribe: (options2) => {
1839
- const subscription = postgreSQLEventStoreSubscription(options2);
1840
- subscriptions.push(subscription);
1841
- return subscription;
1378
+ processor: (options2) => {
1379
+ const processor = postgreSQLProcessor(options2);
1380
+ processors.push(processor);
1381
+ return processor;
1842
1382
  },
1843
1383
  start: () => {
1844
1384
  if (isRunning) return start;
1845
1385
  start = (async () => {
1846
- if (subscriptions.length === 0)
1386
+ if (processors.length === 0)
1847
1387
  return Promise.reject(
1848
1388
  new EmmettError(
1849
- "Cannot start consumer without at least a single subscription"
1389
+ "Cannot start consumer without at least a single processor"
1850
1390
  )
1851
1391
  );
1852
1392
  isRunning = true;
1853
1393
  const startFrom = zipPostgreSQLEventStoreMessageBatchPullerStartFrom(
1854
- await Promise.all(subscriptions.map((o) => o.start(pool.execute)))
1394
+ await Promise.all(processors.map((o) => o.start(pool.execute)))
1855
1395
  );
1856
1396
  return messagePooler.start({ startFrom });
1857
1397
  })();
@@ -1864,11 +1404,472 @@ var postgreSQLEventStoreConsumer = (options) => {
1864
1404
  }
1865
1405
  };
1866
1406
  };
1407
+
1408
+ // src/eventStore/projections/index.ts
1409
+ import "@event-driven-io/dumbo";
1410
+
1411
+ // src/eventStore/projections/pongo/pongoProjectionSpec.ts
1412
+ import "@event-driven-io/dumbo";
1413
+ import {
1414
+ pongoClient
1415
+ } from "@event-driven-io/pongo";
1416
+ var withCollection = (handle, options) => {
1417
+ const { pool, connectionString, inDatabase, inCollection } = options;
1418
+ return pool.withConnection(async (connection) => {
1419
+ const pongo = pongoClient(connectionString, {
1420
+ connectionOptions: { connection }
1421
+ });
1422
+ try {
1423
+ const collection = pongo.db(inDatabase).collection(inCollection);
1424
+ return handle(collection);
1425
+ } finally {
1426
+ await pongo.close();
1427
+ }
1428
+ });
1429
+ };
1430
+ var withoutIdAndVersion = (doc) => {
1431
+ const { _id, _version, ...without } = doc;
1432
+ return without;
1433
+ };
1434
+ var assertDocumentsEqual = (actual, expected) => {
1435
+ if ("_id" in expected)
1436
+ assertEqual(
1437
+ expected._id,
1438
+ actual._id,
1439
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1440
+ `Document ids are not matching! Expected: ${expected._id}, Actual: ${actual._id}`
1441
+ );
1442
+ return assertDeepEqual(
1443
+ withoutIdAndVersion(actual),
1444
+ withoutIdAndVersion(expected)
1445
+ );
1446
+ };
1447
+ var documentExists = (document, options) => (assertOptions) => withCollection(
1448
+ async (collection) => {
1449
+ const result = await collection.findOne(
1450
+ "withId" in options ? { _id: options.withId } : options.matchingFilter
1451
+ );
1452
+ assertIsNotNull(result);
1453
+ assertDocumentsEqual(result, document);
1454
+ },
1455
+ { ...options, ...assertOptions }
1456
+ );
1457
+ var documentsAreTheSame = (documents, options) => (assertOptions) => withCollection(
1458
+ async (collection) => {
1459
+ const result = await collection.find(
1460
+ "withId" in options ? { _id: options.withId } : options.matchingFilter
1461
+ );
1462
+ assertEqual(
1463
+ documents.length,
1464
+ result.length,
1465
+ "Different Documents Count than expected"
1466
+ );
1467
+ for (let i = 0; i < documents.length; i++) {
1468
+ assertThatArray(result).contains(documents[i]);
1469
+ }
1470
+ },
1471
+ { ...options, ...assertOptions }
1472
+ );
1473
+ var documentsMatchingHaveCount = (expectedCount, options) => (assertOptions) => withCollection(
1474
+ async (collection) => {
1475
+ const result = await collection.find(
1476
+ "withId" in options ? { _id: options.withId } : options.matchingFilter
1477
+ );
1478
+ assertEqual(
1479
+ expectedCount,
1480
+ result.length,
1481
+ "Different Documents Count than expected"
1482
+ );
1483
+ },
1484
+ { ...options, ...assertOptions }
1485
+ );
1486
+ var documentMatchingExists = (options) => (assertOptions) => withCollection(
1487
+ async (collection) => {
1488
+ const result = await collection.find(
1489
+ "withId" in options ? { _id: options.withId } : options.matchingFilter
1490
+ );
1491
+ assertThatArray(result).isNotEmpty();
1492
+ },
1493
+ { ...options, ...assertOptions }
1494
+ );
1495
+ var documentDoesNotExist = (options) => (assertOptions) => withCollection(
1496
+ async (collection) => {
1497
+ const result = await collection.findOne(
1498
+ "withId" in options ? { _id: options.withId } : options.matchingFilter
1499
+ );
1500
+ assertIsNotNull(result);
1501
+ },
1502
+ { ...options, ...assertOptions }
1503
+ );
1504
+ var expectPongoDocuments = {
1505
+ fromCollection: (collectionName) => {
1506
+ return {
1507
+ withId: (id) => {
1508
+ return {
1509
+ toBeEqual: (document) => documentExists(document, {
1510
+ withId: id,
1511
+ inCollection: collectionName
1512
+ }),
1513
+ toExist: () => documentMatchingExists({
1514
+ withId: id,
1515
+ inCollection: collectionName
1516
+ }),
1517
+ notToExist: () => documentDoesNotExist({
1518
+ withId: id,
1519
+ inCollection: collectionName
1520
+ })
1521
+ };
1522
+ },
1523
+ matching: (filter2) => {
1524
+ return {
1525
+ toBeTheSame: (documents) => documentsAreTheSame(documents, {
1526
+ matchingFilter: filter2,
1527
+ inCollection: collectionName
1528
+ }),
1529
+ toHaveCount: (expectedCount) => documentsMatchingHaveCount(expectedCount, {
1530
+ matchingFilter: filter2,
1531
+ inCollection: collectionName
1532
+ }),
1533
+ toExist: () => documentMatchingExists({
1534
+ matchingFilter: filter2,
1535
+ inCollection: collectionName
1536
+ }),
1537
+ notToExist: () => documentDoesNotExist({
1538
+ matchingFilter: filter2,
1539
+ inCollection: collectionName
1540
+ })
1541
+ };
1542
+ }
1543
+ };
1544
+ }
1545
+ };
1546
+
1547
+ // src/eventStore/projections/pongo/projections.ts
1548
+ import {
1549
+ pongoClient as pongoClient2
1550
+ } from "@event-driven-io/pongo";
1551
+ var pongoProjection = ({
1552
+ handle,
1553
+ canHandle
1554
+ }) => postgreSQLProjection({
1555
+ canHandle,
1556
+ handle: async (events, context) => {
1557
+ const { connectionString, client } = context;
1558
+ const pongo = pongoClient2(connectionString, {
1559
+ connectionOptions: { client }
1560
+ });
1561
+ await handle(events, {
1562
+ ...context,
1563
+ pongo
1564
+ });
1565
+ }
1566
+ });
1567
+ var pongoMultiStreamProjection = (options) => {
1568
+ const { collectionName, getDocumentId, canHandle } = options;
1569
+ return pongoProjection({
1570
+ handle: async (events, { pongo }) => {
1571
+ const collection = pongo.db().collection(collectionName);
1572
+ for (const event of events) {
1573
+ await collection.handle(getDocumentId(event), async (document) => {
1574
+ return "initialState" in options ? await options.evolve(
1575
+ document ?? options.initialState(),
1576
+ event
1577
+ ) : await options.evolve(
1578
+ document,
1579
+ event
1580
+ );
1581
+ });
1582
+ }
1583
+ },
1584
+ canHandle
1585
+ });
1586
+ };
1587
+ var pongoSingleStreamProjection = (options) => {
1588
+ return pongoMultiStreamProjection({
1589
+ ...options,
1590
+ getDocumentId: options.getDocumentId ?? ((event) => event.metadata.streamName)
1591
+ });
1592
+ };
1593
+
1594
+ // src/eventStore/projections/postgresProjectionSpec.ts
1595
+ import {
1596
+ dumbo as dumbo2
1597
+ } from "@event-driven-io/dumbo";
1598
+ import { v4 as uuid5 } from "uuid";
1599
+ var PostgreSQLProjectionSpec = {
1600
+ for: (options) => {
1601
+ {
1602
+ const { projection: projection2, ...dumoOptions } = options;
1603
+ const { connectionString } = dumoOptions;
1604
+ return (givenEvents) => {
1605
+ return {
1606
+ when: (events, options2) => {
1607
+ const allEvents = [];
1608
+ const run = async (pool) => {
1609
+ let globalPosition = 0n;
1610
+ const numberOfTimes = options2?.numberOfTimes ?? 1;
1611
+ for (const event of [
1612
+ ...givenEvents,
1613
+ ...Array.from({ length: numberOfTimes }).flatMap(() => events)
1614
+ ]) {
1615
+ const metadata = {
1616
+ globalPosition: ++globalPosition,
1617
+ streamPosition: globalPosition,
1618
+ streamName: `test-${uuid5()}`,
1619
+ eventId: uuid5()
1620
+ };
1621
+ allEvents.push({
1622
+ ...event,
1623
+ metadata: {
1624
+ ...metadata,
1625
+ ..."metadata" in event ? event.metadata ?? {} : {}
1626
+ }
1627
+ });
1628
+ }
1629
+ await pool.withTransaction(
1630
+ (transaction) => handleProjections({
1631
+ events: allEvents,
1632
+ projections: [projection2],
1633
+ connection: {
1634
+ connectionString,
1635
+ transaction
1636
+ }
1637
+ })
1638
+ );
1639
+ };
1640
+ return {
1641
+ then: async (assert, message) => {
1642
+ const pool = dumbo2(dumoOptions);
1643
+ try {
1644
+ await run(pool);
1645
+ const succeeded = await assert({ pool, connectionString });
1646
+ if (succeeded !== void 0 && succeeded === false)
1647
+ assertFails(
1648
+ message ?? "Projection specification didn't match the criteria"
1649
+ );
1650
+ } finally {
1651
+ await pool.close();
1652
+ }
1653
+ },
1654
+ thenThrows: async (...args) => {
1655
+ const pool = dumbo2(dumoOptions);
1656
+ try {
1657
+ await run(pool);
1658
+ throw new AssertionError("Handler did not fail as expected");
1659
+ } catch (error) {
1660
+ if (error instanceof AssertionError) throw error;
1661
+ if (args.length === 0) return;
1662
+ if (!isErrorConstructor(args[0])) {
1663
+ assertTrue(
1664
+ args[0](error),
1665
+ `Error didn't match the error condition: ${error?.toString()}`
1666
+ );
1667
+ return;
1668
+ }
1669
+ assertTrue(
1670
+ error instanceof args[0],
1671
+ `Caught error is not an instance of the expected type: ${error?.toString()}`
1672
+ );
1673
+ if (args[1]) {
1674
+ assertTrue(
1675
+ args[1](error),
1676
+ `Error didn't match the error condition: ${error?.toString()}`
1677
+ );
1678
+ }
1679
+ } finally {
1680
+ await pool.close();
1681
+ }
1682
+ }
1683
+ };
1684
+ }
1685
+ };
1686
+ };
1687
+ }
1688
+ }
1689
+ };
1690
+ var eventInStream = (streamName, event) => {
1691
+ return {
1692
+ ...event,
1693
+ metadata: {
1694
+ ...event.metadata ?? {},
1695
+ streamName: event.metadata?.streamName ?? streamName
1696
+ }
1697
+ };
1698
+ };
1699
+ var eventsInStream = (streamName, events) => {
1700
+ return events.map((e) => eventInStream(streamName, e));
1701
+ };
1702
+ var newEventsInStream = eventsInStream;
1703
+ var assertSQLQueryResultMatches = (sql7, rows) => async ({ pool: { execute } }) => {
1704
+ const result = await execute.query(sql7);
1705
+ assertThatArray(rows).containsExactlyInAnyOrder(result.rows);
1706
+ };
1707
+ var expectSQL = {
1708
+ query: (sql7) => ({
1709
+ resultRows: {
1710
+ toBeTheSame: (rows) => assertSQLQueryResultMatches(sql7, rows)
1711
+ }
1712
+ })
1713
+ };
1714
+
1715
+ // src/eventStore/projections/index.ts
1716
+ var handleProjections = async (options) => {
1717
+ const {
1718
+ projections: allProjections,
1719
+ events,
1720
+ connection: { transaction, connectionString }
1721
+ } = options;
1722
+ const eventTypes = events.map((e) => e.type);
1723
+ const projections = allProjections.filter(
1724
+ (p) => p.canHandle.some((type) => eventTypes.includes(type))
1725
+ );
1726
+ const client = await transaction.connection.open();
1727
+ for (const projection2 of projections) {
1728
+ await projection2.handle(events, {
1729
+ connectionString,
1730
+ client,
1731
+ transaction,
1732
+ execute: transaction.execute
1733
+ });
1734
+ }
1735
+ };
1736
+ var postgreSQLProjection = (definition) => projection(definition);
1737
+ var postgreSQLRawBatchSQLProjection = (handle, ...canHandle) => postgreSQLProjection({
1738
+ canHandle,
1739
+ handle: async (events, context) => {
1740
+ const sqls = await handle(events, context);
1741
+ await context.execute.batchCommand(sqls);
1742
+ }
1743
+ });
1744
+ var postgreSQLRawSQLProjection = (handle, ...canHandle) => postgreSQLRawBatchSQLProjection(
1745
+ async (events, context) => {
1746
+ const sqls = [];
1747
+ for (const event of events) {
1748
+ sqls.push(await handle(event, context));
1749
+ }
1750
+ return sqls;
1751
+ },
1752
+ ...canHandle
1753
+ );
1754
+
1755
+ // src/eventStore/postgreSQLEventStore.ts
1756
+ var defaultPostgreSQLOptions = {
1757
+ projections: [],
1758
+ schema: { autoMigration: "CreateOrUpdate" }
1759
+ };
1760
+ var PostgreSQLEventStoreDefaultStreamVersion = 0n;
1761
+ var getPostgreSQLEventStore = (connectionString, options = defaultPostgreSQLOptions) => {
1762
+ const poolOptions = {
1763
+ connectionString,
1764
+ ...options.connectionOptions ? options.connectionOptions : {}
1765
+ };
1766
+ const pool = "dumbo" in poolOptions ? poolOptions.dumbo : dumbo3(poolOptions);
1767
+ let migrateSchema;
1768
+ const autoGenerateSchema = options.schema?.autoMigration === void 0 || options.schema?.autoMigration !== "None";
1769
+ const ensureSchemaExists = () => {
1770
+ if (!autoGenerateSchema) return Promise.resolve();
1771
+ if (!migrateSchema) {
1772
+ migrateSchema = createEventStoreSchema(pool);
1773
+ }
1774
+ return migrateSchema;
1775
+ };
1776
+ const inlineProjections = (options.projections ?? []).filter(({ type }) => type === "inline").map(({ projection: projection2 }) => projection2);
1777
+ const preCommitHook = inlineProjections.length > 0 ? (events, { transaction }) => handleProjections({
1778
+ projections: inlineProjections,
1779
+ connection: {
1780
+ connectionString,
1781
+ transaction
1782
+ },
1783
+ // TODO: Add proper handling of global data
1784
+ // Currently it's not available as append doesn't return array of global position but just the last one
1785
+ events
1786
+ }) : void 0;
1787
+ return {
1788
+ schema: {
1789
+ sql: () => schemaSQL.join(""),
1790
+ print: () => console.log(schemaSQL.join("")),
1791
+ migrate: async () => {
1792
+ await (migrateSchema = createEventStoreSchema(pool));
1793
+ }
1794
+ },
1795
+ async aggregateStream(streamName, options2) {
1796
+ const { evolve, initialState, read } = options2;
1797
+ const expectedStreamVersion = read?.expectedStreamVersion;
1798
+ let state = initialState();
1799
+ const result = await this.readStream(streamName, options2.read);
1800
+ const currentStreamVersion = result.currentStreamVersion;
1801
+ assertExpectedVersionMatchesCurrent(
1802
+ currentStreamVersion,
1803
+ expectedStreamVersion,
1804
+ PostgreSQLEventStoreDefaultStreamVersion
1805
+ );
1806
+ for (const event of result.events) {
1807
+ if (!event) continue;
1808
+ state = evolve(state, event);
1809
+ }
1810
+ return {
1811
+ currentStreamVersion,
1812
+ state,
1813
+ streamExists: result.streamExists
1814
+ };
1815
+ },
1816
+ readStream: async (streamName, options2) => {
1817
+ await ensureSchemaExists();
1818
+ return readStream(pool.execute, streamName, options2);
1819
+ },
1820
+ appendToStream: async (streamName, events, options2) => {
1821
+ await ensureSchemaExists();
1822
+ const [firstPart, ...rest] = streamName.split("-");
1823
+ const streamType = firstPart && rest.length > 0 ? firstPart : "emt:unknown";
1824
+ const appendResult = await appendToStream(
1825
+ pool,
1826
+ streamName,
1827
+ streamType,
1828
+ events,
1829
+ {
1830
+ ...options2,
1831
+ preCommitHook
1832
+ }
1833
+ );
1834
+ if (!appendResult.success)
1835
+ throw new ExpectedVersionConflictError(
1836
+ -1n,
1837
+ //TODO: Return actual version in case of error
1838
+ options2?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK
1839
+ );
1840
+ return {
1841
+ nextExpectedStreamVersion: appendResult.nextStreamPosition,
1842
+ lastEventGlobalPosition: appendResult.lastGlobalPosition,
1843
+ createdNewStream: appendResult.nextStreamPosition >= BigInt(events.length)
1844
+ };
1845
+ },
1846
+ consumer: (options2) => postgreSQLEventStoreConsumer({ ...options2, pool }),
1847
+ close: () => pool.close(),
1848
+ async withSession(callback) {
1849
+ return await pool.withConnection(async (connection) => {
1850
+ const storeOptions = {
1851
+ ...options,
1852
+ connectionOptions: {
1853
+ connection
1854
+ }
1855
+ };
1856
+ const eventStore = getPostgreSQLEventStore(
1857
+ connectionString,
1858
+ storeOptions
1859
+ );
1860
+ return callback({
1861
+ eventStore,
1862
+ close: () => Promise.resolve()
1863
+ });
1864
+ });
1865
+ }
1866
+ };
1867
+ };
1867
1868
  export {
1868
- DefaultPostgreSQLEventStoreSubscriptionBatchSize,
1869
- DefaultPostgreSQLEventStoreSubscriptionPullingFrequencyInMs,
1869
+ DefaultPostgreSQLEventStoreProcessorBatchSize,
1870
+ DefaultPostgreSQLEventStoreProcessorPullingFrequencyInMs,
1870
1871
  PostgreSQLEventStoreDefaultStreamVersion,
1871
- PostgreSQLEventStoreSubscription,
1872
+ PostgreSQLProcessor,
1872
1873
  PostgreSQLProjectionSpec,
1873
1874
  addDefaultPartition,
1874
1875
  addEventsPartitions,
@@ -1905,17 +1906,17 @@ export {
1905
1906
  pongoSingleStreamProjection,
1906
1907
  postgreSQLEventStoreConsumer,
1907
1908
  postgreSQLEventStoreMessageBatchPuller,
1908
- postgreSQLEventStoreSubscription,
1909
+ postgreSQLProcessor,
1909
1910
  postgreSQLProjection,
1910
1911
  postgreSQLRawBatchSQLProjection,
1911
1912
  postgreSQLRawSQLProjection,
1912
1913
  readLastMessageGlobalPosition,
1913
1914
  readMessagesBatch,
1915
+ readProcessorCheckpoint,
1914
1916
  readStream,
1915
- readSubscriptionCheckpoint,
1916
1917
  sanitizeNameSQL,
1917
1918
  schemaSQL,
1918
- storeSubscriptionCheckpoint,
1919
+ storeProcessorCheckpoint,
1919
1920
  storeSubscriptionCheckpointSQL,
1920
1921
  streamsTable,
1921
1922
  streamsTableSQL,