Aggregate Root
These refer to domain context classes which correspond to a cosmos db model schema. Each collection in database should have an aggregate root in the domain layer of the app.
Cosmos DB Model
Above is an example of a simple mongodb schema model for the Service aggregate root in owner community. Each aggregate root requires a schema file in /src/infrastructure-services-impl/datastore/mongodb/models/
Each schema file requires a type interface named after the aggregate root which extends the seedwork Base type interface. This interface should contain all of the required fields laid out in the schema specs.
In the same file, you also need to define a SchemaModel which uses the model function and Schema constructor from mongoose. The same fields defined in the interface need to be provided in the Schema constructor.
This is where the field-level constraints on the schema go. These should be provided in the document you are working off for for the schema specs. Typically, we put required flag here, max length requirements and it's also used to refer to reference field types, subdocument types, and nested path types.
There is an additional object to set timestamps and versionKey. timestamps provides createdAt and updatedAt fields automatically so no need to define them on the type interface or schema model. versionKey adds an _v field on the document for versioning.
You can optionally index fields and set unique constraints at the end.
Primitive Fields
For primitive fields, these are straightforward typings. If the field is optional on the schema specs, you can mark it optional on the interface using the question mark notation.
Reference Fields
Reference fields refer to fields which store an ObjectID to another aggregate root in a different collection. In order to type this on the interface, we use a union typing of PopulatedDoc and ObjectID, both coming from mongoose. Inside of the PopulatedDoc, we pass in the imported schema of the aggregate root the reference field refers to. In this case, we import the Community schema from the community models file.
Note the typing of ObjectId coming from mongoose and the additional field for ref which is using the model imported from the Community schema file.
Nested Path Fields
For nested path fields, you will need to define another type interface for that schema object. See below for an example of the interface type signature. Make sure to extend NestedPath from a seedwork file.
You will need to define both a type interface and a schema model type for each nested path field. The same rules apply for the interface and schema model as for aggregate roots. Follow the same patterns depending on the types of fields defined on the nested path object.
On the aggregate root object, you can specify the type as the schema type you created for the nested path as shown above. Make to sure to include the NestedPathOptions imported from a seedwork file to remove the _id field from all nested path objects.
Note: NestedPathOptions needs to be provided for every nested path, even sub nested path fields as demonstrated with requestedChanges in ServiceTicketRevisionRequestType in the example above.
Subdocument Fields
Subdocument fields are fields that are arrays which contain objects instead of primitive types. A subdocument is necessary instead of a nested path as we need an _id field on each object in the array to be able to keep track of them and use them for updating the correct object in the array.
On the aggregate root type interface, we use Types.DocumentArray from mongoose and similar to nested path fields, we need to define a type interface and schema model type for each subdocument.
See below for an example subdocument type interface and schema model. Note that type interface extends SubdocumentBase from a seedwork file and the schema model use the Schema constructor from mongoose similar to aggregate roots.
Also be sure to include the required id field, which is of type ObjectId coming from mongoose, on all subdocument type interfaces
On the aggregate root schema model, you can simply wrap the schema model for the subdocument in square brackets to set the type of the subdocument field.
File Requirements
Each aggregate root requires:
Domain context file
Domain context files for subdocuments (Entity) and nested paths (ValueObject)
Domain repository file
Domain unit of work file
Domain adapter file
Domain adapter classes for subdocuments (Entity) and nested paths (ValueObject)
Mongo repository file
Mongo unit of work file
Domain Context
Convention: <aggregate-root>.ts
Each domain context corresponds to a schema interface, and contains one Props type interface, one EntityReference interface and one domain context class.
The naming conventions for each are:
<aggregate-root>Props
<aggregate-root>EntityReference
<aggregate-root>
<aggregate-root> in the example above would be Physician.
Props
Here is an example of how to setup a Props interface for any domain context class, whether it be an AggregateRoot, Entity, or a ValueObject. The idea is we are matching the schema model defined in infrastructure layer one-to-one. So each field on the schema needs to be added on the Props interface.
Primitive Fields
For primitive fields such as strings, numbers, booleans, and Dates, we can type them as normal primitives on the Props interface. Examples include title, description, priority, and createdAt.
Reference Fields
For reference fields, which contain an ObjectID which refers to an aggregate root of another type, we type that field using the Props interface of that domain object class and set it to readonly.
For example, in the interface above, the community field is a reference field on ServiceTicketV1 to the Community class. So on the interface, we set community field as readonly and give it the type CommunityProps, which is imported from the domain context file for the community aggregate root.
We also need to include an additional field on the Props interface for reference fields. This field is a function which follows the naming convention of set<ReferenceFieldName>Ref
The function takes in an entity reference of the field it is assigning. In the example above, this extra field is called setCommunityRef and its type is (community: CommunityEntityReference) => void
Prop Array Fields
For prop array fields, we need to set the field as readonly and type it with the PropArray type which takes in Props type interface of the object being stored in the array. In the example above, the messages field is set as readonly and typed with PropArray<ServiceTicketV1MessageProps>
Nested Path Fields
For nested path fields, we need to set the field as readonly and type it with the Props type interface for that nested path's domain context file. In the example above, revisionRequest is a nested path so we set it to readonly and assign it the ServiceTicketV1RevisionRequestProps type interface, which is imported from the domain context file for the service ticket v1 revision request nested path
Entity Reference
The Entity Reference is a required type interface which strips off complex fields like reference fields, nested path fields, and prop array fields.
Here is an example of how to setup the ServiceTicketV1EntityReference based on the ServiceTicketV1Props type interface from above.
Reference Fields
For reference fields, omit the reference field and set<ReferenceFieldName>Ref field from the props interface. Inside of the interface, define a readonly field with the same name as the reference field and assign it to the EntityReference type of the domain context class it refers to.
In the above example, community is a reference field so we omit 'community' and 'setCommunityRef' from the ServiceTicketV1Props interface and we have a readonly community field assigned to CommunityEntityReference type.
Prop Array Fields
For prop array fields, omit the field from the props interface. Inside of the interface, define a readonly field with the same name as the object array field, assign it to the type of PropArray, and pass in the entity reference of the object being stored in that prop array.
In the example above, messages is an object array field so we omit 'messages' from the ServiceTicketV1Props interface, and inside of the interface, we define a readonly messages field and assign it to the type PropArray<ServiceTicketV1MessageEntityReference>
Nested Path Fields
For nested path fields, omit the field from the props interface and inside of the interface, define a readonly field with the same name as the nested path field and assign it the type of the entity reference of the object being stored in that nested path.
In the example above, revisionRequest is a nested path field so we omit 'revisionRequest' from the ServiceTicketV1Props interface, and inside of the interface, we define a readonly revisionRequest field and assign it the type of ServiceTicketV1RevisionRequestEntityReference
Class
The domain context class contains the business logic for updating fields for that schema model. They contain getter and setter methods for the fields defined on the props interface.
For aggregate root domain context classes, the class extends a seedwork class called AggregateRoot, as shown above. It also types the Props interface as a generic on the class. The class also implements the Entity Reference interface.
Each class will also have a few private fields on the class and constructor. Each class needs a context and a visa for permissions checks. An additional boolean field called isNew is required and is initially set to false. This flag is used to bypass some permissions checks on certain fields that are set in the static getNewInstance method.
Static getNewInstance Method
Each Aggregate Root class requires a static getNewInstance method which will always take in newProps which are typed to the Props interface for this domain context and a context which is of type DomainExecutionContext. The rest of the parameters are dependent on the required fields on the schema model that are necssary to a create the object and save it to database.
Inside of the method, the constructor for the domain context is invoked passing in the props and context. Then, it calls the private MarkAsNew method which toggles the isNew boolean flag which allows for the following setters to bypass the usual permissions checks. The MarkAsNew method may also add an integration event related to that domain object's creation. Once all of the required setters are called, the isNew flag is toggled back to false and the new domain context class instance is returned.
Getters
Each field on the Props interface requires a getter method defined in the domain context class. All getters are named to match a field on the props. For most fields, the getter will simply return the field on the props. Other fields require special exceptions for their getter methods.
Primitive Fields
For primitive fields, simply return the field on the props.
Above is an example for the primitive field description which is of type string
Reference Fields
For reference fields, you need to import the domain context class that the reference fields returns to, and pass the props field and context into that constructor, which is what is returned in the getter.
The above example shows the getter method for the reference field community which is of type CommunityProps. The Community domain context class is imported and its constructor is invoked with this.props.community and this.context
Note: Some constructors may require the context, some may require the visa. Some constructors could even require both. Ideally, all constructors should take a visa for permissions checks, and any domain context class with nested path, reference, or prop array fields will require a context.
Nested Path Fields
For nested path fields, the same rule applies as above for reference fields.
Above example is for a nested path field called listingDetail which is of type PropertyListingDetailProps
Optional Reference/Nested Path Fields
For optional reference and optional nested path fields, the same rule applies. In the case of this.props.<field> being undefined, we don't want to pass those props into the constructor. So the getter needs to use ternary operator to test if this.props.<field> is defined. If it is, return the constructor invocation as usual. Otherwise, return undefined.
In the above example, revisionRequest is an optional nested path of type ServiceTicketV1RevisionRequestProps.
Prop Array Fields
For prop array fields, we need to map over the props field and return each element of the array as a constructor for the domain context class of the object stored in that array.
In the example above, messages is a prop array field of type ServiceTicketV1MessageProps so the constructor calls map on this.props.messages and returns a new ServiceTicketV1Message for each element contained in the array.
Note: These are the only getters we include a type on, which is ReadonlyArray with the domain context class of the array elements passed in.
Setters
Similar to getters, we need setters for most of the fields on the Props interface. Above are a few examples of setter methods. These contain the business logic that drives the permissions and constraints of our applications.
They begin with a condition usually checking the visa for various permissions specific to each project which determine whether the caller of the mutation updating that specific field is allowed to do so.
When first setting up the project, these permissions most likely will not exist, so for the first pass, the setter methods can be written without the visa permission checks.
The second half of the business logic is contained in our Value Objects. We use an npm package called value-objects which allows us to create wrapper types around primitives to set constraints on any individual field, such as max length, enum values, or regex patterns.
The convention for our setter methods is the field name capitalized and the argument is the field name with the primitive type it is assigned to. When assigning the new value to the props, the argument is passed in to the corresponding Value Object constructor for that field, which validates it against the field-level constraints, and call valueOf() method to get back the underlying primitive type to save to the props.
If Value Objects haven't been set up yet, just set the prop field to the argument directly for now.
Note: Not all fields will need value objects. Depends on business requirements and the validation set on the schema model.
Primitive Fields
Reference Fields
For reference fields, the setter method takes in an Entity Reference as its argument and instead of setting this.props.<referenceField>, it uses the set<ReferenceField>Ref method and passes in the argument parameter to that function.
Prop Array Fields
For prop array fields, we don't have a direct setter method for the array field. Instead, we have special request methods which create a new instance of the array element and return it, as well as a method for adding a new element to the prop array.
So we need to create two methods. First, a private requestNew<PropArrayField> method which invokes getNewItem() method on this.props.<propArrayField> and returns a constructor invocation passing in the new props, context and visa into the domain context class that the prop array holds.
The second method is the public facing setter method called requestAdd<PropArrayField> which calls the private requestNew method and uses the arguments to set the required fields on the new array element. Which fields need to be set varies for each domain context class.
Nested Path Fields
For nested path fields, these are actually exempt from setters and have no need for one as the domain context field for the nested path contains the setter methods for its underlying fields, so the parent class only needs a getter method for nested paths to access those setters.
onSave override
Some aggregate roots may have an associated domain event which is tied to any updates on that domain context class. In order to trigger this event, the domain context class may override its parent onSave method to add an integration event for the updated event for that domain object.
Again, this will not be necessary for all aggregate roots. Typically this only applies to aggregate roots which are a part of cognitive search collections and need to trigger a reindex when any fields are updated.
Domain Repository
Each Aggregate Root requires a Domain Repository file. This file goes in the same folder as the domain context class file. The filename convention is <aggregate-root>.repository.ts
The Domain Repository is an interface which uses the Props interface from Domain Context file and a seedwork Repository class which also takes in the props and domain context class.
The exact methods to be implemented for each repository can be specific to each aggregate root. For the initial setup, put the method signature for the static getNewInstance() method, excluding for the props and context arguments. You can also define a getById() method which returns the domain context class type.
Domain Unit of Work
Each Aggregate Root also requires a Domain Unit of Work file in the same folder as the domain context class file. The filename convention is <aggregate-root>.uow.ts
The Domain Unit of Work is an empty interface which extends the seedwork class UnitOfWork and takes in various files from the aggregate root's domain context folder.
Infrastructure
In /src/infrastructure-services-impl/datastore/mongodb/infrastructure/, create a folder for your aggregate root named after the collection. Inside will be:
<aggregate-root>.domain-adapter.ts
<aggregate-root>.mongo-repository.ts
<aggregate-root>.uow.ts
Domain Adapter
Each aggregate root requires a domain adapter file for converting from domain to persistence. The Domain Adapter file contains a <AggregateRoot>Converter class which extends the MongoTypeConverter seedwork class. There is also the <AggregateRoot>DomainAdapter class which extends the MongooseDomainAdapter seedwork class.
Each domain adapter must provide getters and setter methods for each of the fields defined on the Props interface for that domain context class. The convention in domain adapter files is to put the getter and setter for a field together in the code, as shown in the example above.
Primitive Fields
For primitive getters, simply return the field on the document.
For the primitive setters, just assign the input parameter to the document field to set it.
Reference Fields
For reference field getters, even if the field is required, we have a conditional check to ensure that the underlying field is defined before passing it into the constructor for the domain adapter for that aggregate root.
In the example above, community is a reference field to the aggregate root Community so we pass this.doc.community into the CommunityDomainAdapter class so long as it isn't undefined.
For reference field setters, instead of the usual set method, here is where we implement the special set<ReferenceFieldName>Ref method we defined on the Props interface in domain layer.
We call this.doc.set() where the first argument is a string of the reference field name on the document and the second argument is the community input with 'props' and 'doc' selected with square bracket notation.
Optional Reference Fields
For optional reference fields, the getter is the same as above. However for the setter, since we are allowing the field to be cleared of its value, we need to handle the input parameter being undefined. If the argument is defined, we set the document how we usually do for reference fields. If not, we set the document field to null.
Nested Path Fields
For nested path fields, we only need to provide a getter method, similar to the domain context class. For both optional and required nested path fields, we first check if the document field is undefined. If true, we set the document field to an empty object to initialize it. Otherwise, we pass the document field into a domain adapter class for that nested path object.
Note: You will have to create Domain Adapter classes for all nested paths (and subdocuments) in an aggregate root similar to the domain context files. All sub domain adapters should be put in the same file as the aggregate root domain adapter they are a part of.
Prop Array Fields
For prop array fields, we also only need to provide a getter method. Return the constructor invocation for the MongoosePropArray seedwork file and pass in the document field as well as a domain adapter class for the subdocument that the array contains.
Note: Same as for nested path fields, you will need to create a domain adapter class for prop array field that provides the getters and setters for the underlying object held in the prop array. These also go in the same file as the aggregate root domain adapter.
Mongo Repository
The Mongo Repository class extends a MongoRepositoryBase seedwork class and implements the Domain Repository interface for a given aggregate root. In the example above, ServiceTicket is imported from the mongodb schema models from infrastructure layer and ServiceTicketDO is an alias for the ServiceTicket domain context class from domain layer.
The Mongo Repository implements the methods defined on the Domain Repository interface. Follow the example above for both implementations. Ignore the populate() call in the getById() method, just return the result of findById() for now.
Mongo Unit of Work
The Mongo Unit of Work for each aggregate root requires the type converter from the domain adapter file, the model from the mongodb schema file, and the mongo repository. It also imports a few seedwork classes.
Last updated