Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reshaped MongoDBEventStore initialisation #136

Merged
merged 4 commits into from
Dec 10, 2024

Conversation

oskardudycz
Copy link
Collaborator

@oskardudycz oskardudycz commented Dec 6, 2024

1. Reshaped MongoDBEventStore initialisation to allow passing client or connection string.

That should make it more accessible.

Internally, it'll ensure that the client is connected and allow closing the client through the exposed close method if it was created by the event store.

Also hidden is the class and exposed type to replace MongoDBEventStore with in-memory implementation for integration tests.

Passing clients will enable more advanced features further on, like sessions, transactions, etc.

Having collection per stream type will also make easier indexing of inline projections and other optimisations.

2. Renamed events to messages

In the longer term, we want to store also commands sometimes, not only events. This change should stabilise the payload.

Reshaped MongoDBEventStore initialisation to allow passing client or connection string. That should make it more accessible.

Internally it'll ensure that client is connected, and allow closing client through exposed close method if it was created by the event store.

Hidden also class and exposed type to allow replacing MongoDBEventStore by inMemory implementation for integration tests.

Passing client will enable more advanced features further on like sessions, transactions, etc.

Having collection per stream type will also make easier indexing of inline projections and other optimisations.
@oskardudycz
Copy link
Collaborator Author

oskardudycz commented Dec 6, 2024

@alex-laycalvert, could you have a look? 🙏 🙂

In the longer term, we want to store also commands sometimes, not only events. This change should stabilise the payload.
@oskardudycz oskardudycz force-pushed the mongodb_event_store_initialisation branch from f4ee088 to 455e3ec Compare December 6, 2024 13:36
@oskardudycz oskardudycz force-pushed the mongodb_event_store_initialisation branch from 02f99c9 to 2f6d2eb Compare December 6, 2024 15:23
@@ -246,13 +284,13 @@ export class MongoDBEventStore implements EventStore<MongoDBReadEventMetadata> {
readModels: stream?.projections ?? {},
events: eventsToAppend,
projections: this.inlineProjections,
collection: this.collection,
collection,
updates,
client: {},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see this on the updates to projections, but for inline projections a client isn't necessary right? Or is it just because the context for inline projections and async projections (once implemented) will be the same type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true: for now. If we have a strategy to have a collection per stream type, then technically, this could be placed in a separate Mongo database. For that, we'd need the client to resolve the collection. Having collection in different databases would help to have e.g. separation of data storage per module or per tenant and still use a single event store singleton setup.

private readonly client: MongoClient;
private readonly defaultOptions: {
database: string | undefined;
collection: string | undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add some explicit documentation that not providing a collection would have each stream type separated into a separate coll., but providing a collection would only use that for everything. Just to avoid it not being obvious that this changes how the streams are stored.

Maybe we also allow for a collectionFor option to allow the dev the option to deal with this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add some explicit documentation that not providing a collection would have each stream type separated into a separate coll., but providing a collection would only use that for everything. Just to avoid it not being obvious that this changes how the streams are stored.

Yes, that's a good call, what would you say if we force user to make an explicit choice through the option and have event store options that'd have a field in event store option { storage: 'SINGLE_COLLECTION' } or { storage: 'COLLECTION_PER_STREAM_TYPE' } , that'd allow to discriminate user choice and force to e.g. provide collection for the first option, and client for the second (and having that mutually exclusive).

Maybe we also allow for a collectionFor option to allow the dev the option to deal with this.

Just to double-check: would you give user an option to provide their own resolution function through store options? I like the idea!

Copy link
Collaborator

@alex-laycalvert alex-laycalvert Dec 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah choosing single/multi collection storage I think is the way to go. I think something like this:

type Options =
    | { type: 'SINGLE_COLLECTION', collection?: string }
    | {
        type: 'COLLECTION_PER_STREAM_TYPE',
        collectionFor?: (streamType: string) => Promise<Collection> | Collection
    }

I think it can be the same type def as the private collectionFor. Could allow for grouping certain stream types together into the same collection, not that it's necessarily useful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added issue for that: #142

if (this.isClosed) return Promise.resolve();

this.isClosed = true;
if (!this.shouldManageClientLifetime) return Promise.resolve();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should add some docs either on close or on the client option to make sure the user knows that we only attempt to close the client if we are managing it. Looking at the code I can see this and understand why but I can imagine a scenario where a user attempts to close the event store but they had passed a client, and even though this.isClosed is true, the client is still open and calling this function has no effect on it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also want to add some checks for isClosed in collectionFor or the individual functions for interacting with the event store. I think if the client is actually closed it'll throw an error when you try to use it and it's been closed, but in the above example, you could technically call close on the eventstore even if you passed your own client and it essentially does nothing.

I might be missing something obvious in why this was intended, just my thoughts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should add some docs either on close or on the client option to make sure the user knows that we only attempt to close the client if we are managing it.

Yes, makes sense. Besides docs, I could also make some typescript Voodoo with type interference and expose the close method only if the user passed the client through options (so essentially, we'd have different types for managed event store and "ambient" one). But I'm not sure if it'd be more intuitive or not. Currently calling close is a safe default for both options, as it shouldn't cause any side-effects. Thoughts? 🤔

We might also want to add some checks for isClosed in collectionFor or the individual functions for interacting with the event store.

I was thinking on it, but then if someone closed the event store then it'll get the same MongoDB errors as they'd get from a regular usage. But it may be worth indeed wrapping it with some helper and more user-friendly Emmett errors. Thoughts? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense, I think that's fine. Some TS magic to not expose close if client is provided would probably be enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added issue for that: #143

Copy link
Collaborator

@alex-laycalvert alex-laycalvert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might just want to add some end user docs before things get too fancy. It might just be me but there's definitely some functionality in here that's not entirely obvious from what this module exports that could cause unexpected behavior.

@oskardudycz
Copy link
Collaborator Author

@alex-laycalvert thank you a lot for a honest review, that's much appreciated!

@oskardudycz
Copy link
Collaborator Author

@alex-laycalvert, per our agreement on Discord, I've set up issues for the follow-up typing improvements. As they're not blocking, let's get this PR in, and I'll work in the next few days to fill the missing gap.

@oskardudycz oskardudycz merged commit d3303a4 into main Dec 10, 2024
3 of 4 checks passed
@oskardudycz oskardudycz deleted the mongodb_event_store_initialisation branch December 10, 2024 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants