Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Detecting whether an aggregate exists #1589

Closed
oliversturm opened this issue Oct 6, 2020 · 10 comments
Closed

Detecting whether an aggregate exists #1589

oliversturm opened this issue Oct 6, 2020 · 10 comments

Comments

@oliversturm
Copy link

I think it would be useful if there was a built-in mechanism to detect whether an aggregate exists (in a command handler function) - reSolve knows this, since it has just attempted to retrieve the aggregate before the function is called. Unfortunately, the information is not passed on to the command handler and I am left to my own devices to figure it out. Since many aggregates require this logic to avoid duplicates, there is room for improvement here.

Currently, the only "good" option is to handle a special aggregate field which denotes specifically whether the aggregate exists or not. (Note - I'm aware of several other approaches that are worse than this - please let me know in case you need me to elaborate.)

So I'd have this in thing.commands.js:

createThing: (aggregate, { payload: { ... } }) => {
  if (aggregate.exists) throw new Error('Thing exists');
  ...
}

And then this in thing.projection.js:

[THING_CREATED]: (aggregate) => ({
    ...aggregate,
    exists: true,
}),

Obviously this approach works - but I don't think it should be necessary. It's boilerplate, required for almost any aggregate, and all just to detect a piece of information that is already known to reSolve!

I suggest we use one of these two approaches:

  1. Pass a flag to the command function that indicates whether the aggregate is new (i.e. equal to its initial state)
  2. Pass the aggregate version to the command function - this mysteriously turns up in the event structure after the command function completes. I have to admit I don't exactly understand how this version is counted, or why it would be included in the event, but I suspect that the version exists already before the command function is called even for a new aggregate and could therefore be used as an indicator.
@max-vasin
Copy link
Contributor

Hi! Indeed, it looks like boilerplate, but that not quite right. Imagine an aggregate:

createThing: (state, { payload: { ... } }) => {
  if (state.exists) throw new Error('Thing exists');
  ...
}

deleteThing: (state: { payload: { ... } }) => {
	if (!state.exists) throw new Error('Thing not exists')
}

updateThing: (state: { payload: { ... } }) => {
	if (!state.exists) throw new Error('Thing not exists')
	...
}

We need to define exists automatically under the hood. But here some inconsistencies:

  1. If we define exists as equal to initial state and createThing command will not produce any events - deleteThing fails. Moreover - aggregate can fall to state equal to its initial during lifetime.

  2. If we define exists as have at least one event - updateThing can update deleted thing without any exception

So, my opinion, its more looks like a pattern, or somewhat, but not a boilerplate that can be moved to internals.

@superroma
Copy link
Collaborator

superroma commented Oct 8, 2020

First, I would approach this without changing reSolve - for instance, with some kind of high order function.

const wrapFunc = _.pipe(checkCreated('createThing', 'Thing'), preventAnonymousAccess())
export default wrapFunc({
    createThing: ....
    deleteThing: ...
    updateThing: ...
})

If this is something that developers would often forget to do, then we can think of kind of middleware that once registered, will be called before each command handler and projection function

Also, I would not enforce such things by default, I can imagine an aggregate that can have no create* command at all - if system doesnt care if it created or just updated, then it can leave without this command.

@oliversturm
Copy link
Author

I'm not quite sure that my request has been understood correctly. So let me try to clarify two points:

  1. I'm not looking to have an automatic exists field
  2. I don't want to automate the checks so that they always happen

Instead, I want a way to access the information reSolve already holds: is the aggregate passed to the command function a new one (i.e. it has just been created from the initial value and no projections have ever been applied to it) or is it an old one (i.e. has had projections applied)? I believe that reSolve can tell the difference - am I correct?

If so, I could imagine a simple flag passed to the function:

  myCommand: (state, { aggregateId, isNew, payload: ... }) => {
    // check isNew to find out whether the aggregate is new
  }

I'm not sure whether the suggested placement of this flag makes most sense, there might be better ways of passing it. But I hope this serves to clarify what I'm asking.

@superroma
Copy link
Collaborator

Internally aggregate has a version - number of events applied to it before command.
We can expose this version somehow. For a new aggregate it would be 0.

@oliversturm
Copy link
Author

Yes! That's what I said in my initial post:

Pass the aggregate version to the command function - this mysteriously turns up in the event structure after the command function completes

I suddenly forgot about it myself, but I believe this detail would be useful - I would leave it up to the developer to do with what they want, and I also agree with you, Roman, that a collection of higher order functions would be the best approach to make "standard" functionality/patterns available over time.

@max-vasin
Copy link
Contributor

It seems to be undocumented, but current aggregate version is passing within command context (third argument):

doSomething: (state, command, { aggregateVersion }) => {
  ...
}

@oliversturm
Copy link
Author

Wow :)

@oliversturm
Copy link
Author

I tested it to be sure and this approach works exactly like it should:

{
  createThing: (aggregate, { payload: { ... } }, {aggregateVersion}) => {
    if (aggregateVersion > 0) throw new Error('Thing exists');
    ...
  },

  updateThing: (aggregate, { payload: { ... } }, {aggregateVersion}) => {
    if (aggregateVersion === 0) throw new Error('Thing does not exist');
    ...
  },
}

Unless this API changes, or there are reasons why it shouldn't be done this way, this solves my problem. I'll close this issue.

@oliversturm
Copy link
Author

Turns out that a command in aggregate A can be called with an aggregate ID that belongs to an aggregate instance of type B. In such a case, the aggregate version that is passed to the command handler will be that of the B-type instance, but the aggregate instance itself is a new (created from Init) instance of type A. This is in itself rather confusing and strange, but it means that the aggregateVersion cannot be used to distinguish between "new" and "old" aggregates.

@oliversturm oliversturm reopened this Nov 12, 2020
@oliversturm
Copy link
Author

To be clear. Let's say I send a command to create an aggregate A:

aggregateName: 'A', aggregateId: '1', type: 'createA'

Now I send a command to create an aggregate B. I use the same ID as before:

aggregateName: 'B', aggregateId: '1', type: 'createB'

I will now find myself in the function createB on aggregate B. The aggregateVersion will be 1 (because the aggregate with this ID has already been created previously) and the aggregate instance I receive will be a new instance for type B (null or the Init value from type B projection).

In this situation, the aggregateVersion indicates that the aggregate already exists, while the state I have is an initial state. Hence my comment above that the aggregateVersion cannot be used to distinguish between "new" and "old" aggregates.

@reimagined reimagined locked and limited conversation to collaborators Apr 9, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants