-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changing how we deal with resources #118
Conversation
…cally deserializes to correct type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yowza! that was quite a ride 😄. I learned a lot by looking at this PR and spent quite a lot of time with Google opened as I went 😅 . I've left a trail of comments, mostly for observation and some questions. There a few suggestions a well, but none of these is blocking.
I tested with the SampleApp, and that worked nicely. What I didn't get to test is with an existing Vanir code-base. How will an existing codebase update its MongoDB usage? Will all queries need to be re-written, or can those projects continue with TypeGoose / Mongoose if they desire? I assume that they can, but I haven't understood how. Could you write a few words somewhere about that, or point me to those words if I've missed it?
private ThrowIfMissingResourceConfigurationOfType<TResource extends ResourceConfiguration>(resourceConfigurations: Map<Constructor<{}>, any> | undefined, resourceConfigurationType: Constructor<TResource>, tenantId: TenantId) { | ||
if (!resourceConfigurations || !resourceConfigurations.has(resourceConfigurationType)) { | ||
throw new MissingResourceConfigurationOfType(tenantId, resourceConfigurationType); | ||
} | ||
} | ||
|
||
private ThrowIfMissingResourceConfigurationsForTenant(tenantId: TenantId) { | ||
if (!this._resourceConfigurationsByTenants.has(tenantId.toString())) { | ||
throw new MissingResourceConfigurationsForTenant(tenantId); | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seeing these methods are private, should they not be pascalCased?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed they should :) - and not just because they’re private since thats what we do for all properties and methods & functions. A bit surprised we don’t have a lint rule set for this.
The serialization subsystem introduces the concept of a `CustomType` - these are the ones used to convert to and from | ||
MongoDBs BSON binary. | ||
|
||
Creating your own custom type is easy, all you need to do is to implement the abstract class `CustomType`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section is good to have and read about. A question comes to mind (sorry if the answer is obvious): When would one need to create a custome type? For enum's, array's, every complex object to be stored? A little clarification here may be worth adding
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great question - I think the documentation could be clearer on this - I’ll add more clarity around it.
CustomTypes are only for those occassions where you want it stored in a particular way rather than what the default behavior is. If you take the Guid that we have a custom type for, if we didn’t have the custom type - it would by default recursively serialize the class Guid and end up with all the bytes as a byte array plus any other properties on that type. While what we’re looking for for that particular type is a way to be able to query it easily in Mongo and also easily read it (or more easily - Guids are hard to read anyways :)).
A good example of a CustomType we probably would like to have is for the DateTime type we’ve been using. Serializing it by default would end up with a structure like this being saved:
{
«year»: 2021,
«month»: 2,
«day»: 1,
«hour»: 13,
«minute»: 37,
«seconds»: 37,
«milliseconds»: 654
}
While what would be nice is that it ended up like the date type in MongoDB.
I’ve added an issue for the DateTime type : #119
I think for the most part CustomTypes aren’t needed.
Hope that made sense.
These resources are managed for you in the [Dolittle platform](https://dolittle.io/docs/platform/requirements/#1-your-application-must-use-the-resource-system). | ||
And its therefor important that solutions support this. Vanir brings a resource system |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This page in the platform documentation doesn't actually describe what the Dolittle platform defines as resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ex: can we introduce custom resource configurations here that can be used in the app, or are these specific platform resources that are well-defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great question. The platform documentation leaves us the opportunity to change this - as we have plans to improve on how we do things. Today the solution is the resources.json file and it is part of the deployment yaml file - which is not available for modification to anyone outside Dolittle. It is limited to basically EventStore and MongoDB for ReadModels as that are the managed resources we have.
I can add a bit more details on this in this documentation.
@Get() | ||
async doStuff(): Promise<MyType[]> { | ||
const collection = await this._db.collection(MyType); | ||
await collection.insertOne({ | ||
_id: Guid.create(), | ||
something: 42 | ||
}); | ||
const result = await collection.find().toArray(); | ||
return result; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This returns the following result to the frontend over Swagger. Is this right?
{
"something": 42,
"_id": {
"bytes": [
14,
103,
48,
90,
28,
148,
77,
210,
163,
30,
9,
218,
40,
226,
118,
28
],
"_stringVersion": "5a30670e-941c-d24d-a31e-09da28e2761c"
}
},
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is expected behavior. Thats how a Guid type looks like. When using GraphQL we have in Vanir a scalar type (https://github.com/dolittle-entropy/vanir/blob/main/Source/typescript/backend/data/GuidScalar.ts) that converts it back and forth. This way we get to use the Guid type for Guids in code, have it converted using our CustomType
construct for Mongo.
I haven’t looked into TSOA and if its possible to have a similar experience for ReST APIs.
Adding an issue to investigate it (#120)
database: { | ||
host: 'localhost', | ||
database: 'data', | ||
port: 27017 | ||
}, | ||
eventstore: { | ||
host: 'localhost', | ||
database: 'events', | ||
port: 27017 | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the vanir.json file also remove the database
and eventstore
properties? Both for the Sample, and the templates in create-dolittle-microservice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch - indeed it should :)
if (options) { | ||
return db.collection(name, options); | ||
} | ||
const collection = db.collection(name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be simplified to sending in default empty options. Something like this (can also be set as default value in method parameter):
const collection = db.collection(name,options || {});
I don't know if this actually changes the behaviour though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second look of the context of the code - this would change the behavior. This is the whole dance of overloaded methods that creates for a bit of messy implementations. So the condition is basically for a specific overload.
// Copyright (c) Dolittle. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
import { ResourceConfiguration } from '../resources'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the MongoDbReadModelsConfiguration be in the resources folder next to EventStoreConfiguration? Or should the EventStoreConfiguration be moved to another folder, perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think its more the latter. The reason is cohesion - keep all the MongoDb stuff in one place. We just don’t have any EventStore things yet. But I think a better place for the EventStoreConfiguration would be the Dolittle folder. I’ll move that to there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that makes more sense! 👍
this._resourceConfigurationsByTenants.set(tenantId.toString(), resourceConfigurationsMap); | ||
|
||
for (const resourceType in tenantResources) { | ||
switch (resourceType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this answers my question earlier about which resource types the platform supports :D. Should this become a string-enum with well-known things?
import { TenantId } from '@dolittle/sdk.execution'; | ||
|
||
const ContextKey = 'Context'; | ||
const ns = cls.createNamespace('0e852ce9-ef09-45d2-b64b-4868de226563'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is really interesting. It functions as a request cache, right? Is the Guid is just a random guid you put there, or does it connect with something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see it's also used in the mongo db context middleware - didn't notice it before. Nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key can be anything - as long as its unique.
What it does is give us a key/value store per async context - making it safe to store values and be sure you’re using it in the right context when you’re in the same call context. Similar to AsyncLocal in C#/.net.
Our use case is to be able for each web request coming in to set the correct context based on TenantId, UserId coming in and such and make this accessible in a good way throughout different parts of the request.
Its leveraging a project called cls-hooked
for this - which makes it a bit nicer to do. You can read more on it here: https://medium.com/trabe/continuation-local-storage-for-easy-context-passing-in-node-js-2461c2120284
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks like a nice and unobtrusive way of adding this context information. I read the blog post and it links to some performance issues that may be worth considering, as this is adding a significant performance hit according to that Github thread. nodejs/benchmarking#181
You’d also want to stay away from async_hooks if you’re writing a library since you could impact performance for your users just for your convenience.
There doesn't seem to be any improvement or resolution though. Perhaps another approach may be better for this, or we could see how it actually impacts applications? Perhaps creating a follow-up task for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to keep an eye on the performance. I've added an issue (#121) as you suggested.
Great catch! The benchmarks they had there was on version 9 of Node. I think for most of our current scenarios we are ok.
https://github.com/bmeurer/async-hooks-performance-impact
It talks about # of requests a second in the thousands.
app.use(ContextMiddleware); | ||
app.use(MongoDbContextMiddleware); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, tying it all together!
Great feedback and questions. I’ll amend the documentation a bit and also fix the things you pointed out in the code. Thanks! |
To answer your questions related to what we do with solutions using this. That leaves us with the schema part. There are some aspects to it that is interesting. That being said, please come with other perspectives I'm not seeing and if there is something missing in this assumption. |
Summary
This pull request is a major change.
The biggest change is the removal of TypeGoose and Mongoose as default - instead it leverages now the native MongoDB driver and all setup is configured for this.
Added
Changed
Removed