-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
chore(dal): fixed the mongo schema types and repositories to return the correct mapped types #2890
chore(dal): fixed the mongo schema types and repositories to return the correct mapped types #2890
Conversation
…he correct mapped types
NV-1753 The repository function `findById` has wrong return type
Why? (Context)The repository function What?Revisit all the repositories. Definition of DoneThe return type should be like |
import { CreateChange } from '../src/app/change/usecases/create-change/create-change.usecase'; | ||
import { CreateChangeCommand } from '../src/app/change/usecases/create-change/create-change.command'; |
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 the API migration file, I've fixed the imports and ts issues here
super( | ||
messageRepository, | ||
createLogUsecase, | ||
createExecutionDetails, | ||
subscriberRepository, | ||
getDecryptedIntegrationsUsecase | ||
); |
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.
added GetDecryptedIntegrations
as it's required by the super class, check below
@@ -21,7 +21,7 @@ export abstract class SendMessageBase extends SendMessageType { | |||
protected createLogUsecase: CreateLog, | |||
protected createExecutionDetails: CreateExecutionDetails, | |||
protected subscriberRepository: SubscriberRepository, | |||
protected getDecryptedIntegrationsUsecase?: GetDecryptedIntegrations | |||
protected getDecryptedIntegrationsUsecase: GetDecryptedIntegrations |
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 should not be optional as we have the code that uses this use-case
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.
CC. @djabarovgeorge
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's true, I made him optional only because at the moment in-app does not require integration.
|
||
export class BaseRepository<T_Query, T_Response> { | ||
public _model: Model<any & Document>; | ||
export class BaseRepository<T_DBModel, T_MappedEntity> { |
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.
Did a lot of type fixes here (no logic changes), but basically, the BaseRepository
requires now to pass the T_DBModel
which is a DB model, and T_MappedEntity
which is the type that we map the DB object to.
So like the Mongoose model return DBModel
objects and we map them to Entity
, just to have ids as strings, not the ObjectIds.
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 really like to differentiate the type of what the repository is expecting and what is returning. Was a pattern I used a lot in the past. I am happy you went that direction.
export type ChangeDBModel = Omit< | ||
ChangeEntity, | ||
'_creatorId' | '_environmentId' | '_organizationId' | '_entityId' | '_parentId' | ||
> & { | ||
_creatorId: Types.ObjectId; | ||
|
||
_environmentId: Types.ObjectId; | ||
|
||
_organizationId: Types.ObjectId; | ||
|
||
_entityId: Types.ObjectId; | ||
|
||
_parentId?: Types.ObjectId; | ||
}; |
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 a DBModel
that we pass to the Mongoose schema, please note that id fields have types as Types.ObjectId
. So we will have here just to override types for the ids.
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.
Is this the replacement of the query enforcement that @djabarovgeorge implemented?
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 query enforcement was to force using _organizationId
or _environmentId
when querying the model... but as I commented somewhere else this strict rule breaks the types, like the Mongoose model raises TS errors because these fields should be optional
import { ChangeEntityTypeEnum } from '@novu/shared'; | ||
|
||
import type { EnvironmentId } from '../environment'; | ||
import type { OrganizationId } from '../organization'; | ||
|
||
export class ChangeEntity { |
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 type we do return from the repositories to the outside world
export const Change = | ||
(mongoose.models.Change as mongoose.Model<ChangeDBModel>) || mongoose.model<ChangeDBModel>('Change', changeSchema); |
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'm not sure why we have here "or", but I guess it's for the case when the model already is created.
So when the first part is true it means that the model is created so we cast it to the correct type.
The second part creates the model passing the correct type and schema.
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.
It is mainly for e2e testin for the exact reason you have mentioned 🙏
class PartialExecutionDetailsEntity extends Omit(ExecutionDetailsEntity, ['_environmentId', '_organizationId']) {} | ||
|
||
type EnforceEnvironmentQuery = FilterQuery<PartialExecutionDetailsEntity & Document> & | ||
({ _environmentId: string } | { _organizationId: string }); |
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.
Removed these hacks to force the _environmentId
or _organizationId
, because it breaks all the Mongoose types.
When the query is passed to the model function, the query fields are optional, but we force them to be required and that breaks types.
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 was implemented in purpose by @djabarovgeorge. @scopsy already flagged it but what's the replacement for this functionality?
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've answered here: #2890 (comment)
There is no replacement, when you do code the queries you should know which fields you have to use to get the correct DB result...
So the recommendation is when the document has both fields _environmentId, _organizationId
we should query by _environmentId
as it's org specific. In other cases by the fields that are accessible.
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.
We added it mainly to help us (and the community) from missing those fields. While working on the implementation I encountered a couple of those issues in the project that was missing environment fields.
.populate('template', selectTemplate) | ||
.populate('notification', selectNotification) | ||
.populate('subscriber', selectSubscriber) | ||
.populate('environment', selectEnvironment) | ||
.lean() | ||
.exec(); | ||
|
||
return job as unknown as JobEntityPopulated; |
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.
here we don't do mapping, I don't know why, but I see its failing when calling this.mapEntity(job)
and returns {}
empty object
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.
Try to replace this.MongooseModel
for this._model
in line 157 and see if that's the reason.
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.
@p-fernandez that is not the reason, base repository sets the this._model
in the constructor, the same as it does for this.MongooseModel
. The problem might be the JSON.stringify(entity)
call that does the this.mapEntity
@@ -32,6 +32,7 @@ import { CacheKeyPrefixEnum, InvalidateCacheService } from '../../../shared/serv | |||
import { SendMessageBase } from './send-message.base'; | |||
import { ApiException } from '../../../shared/exceptions/api.exception'; | |||
import { OrganizationEntity } from '../../../../../../../libs/dal/src/repositories/organization/organization.entity'; |
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 know not related but this import is bad, need to be refactored to be from @novu/dal, don't we have a lint rule for banned imports 🤔
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 we don't. And this was probably and IDE playing around with a fix with an auto import. It is the only occurrence I could find in a quick search in the codebase.
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'll fix this one, but agree we should enable that rule.
@@ -1,5 +1,6 @@ | |||
// eslint-ignore max-len | |||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | |||
import { AnyKeys } from 'mongoose'; |
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.
Unfortuently we should not use mongoose
related things in API. Can we re export this thing from the dal package? Ideally we would want the application to know as less as possible about mongoose or mongo
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.
Was going to highlight this. Also what's the difference between AnyKeys
type and Partial
the one implemented before?
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.
There is a small difference, Partial
still requires the correct types for values to be passed, but the AnyKeys
allows either for the correct value type or for any other type. So for this file it might work, but I've used it in another place when we do assign the _feedId: null
but the type is string | undefined
, so it raises the issue there, but yeah I'll fix that place too.
|
||
class PartialLogEntity extends Omit(LogEntity, ['_environmentId', '_organizationId']) {} | ||
|
||
type EnforceEnvironmentQuery = FilterQuery<PartialLogEntity & Document> & |
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.
How the enforcement is happening right now? Maybe just missed it
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.
answered here: #2890 (comment)
@@ -181,7 +182,7 @@ export class GetChanges { | |||
entityId: string, | |||
environmentId: string | |||
): Promise<IViewEntity | Record<string, unknown>> { | |||
let item = await this.layoutRepository.findOne({ | |||
let item: LayoutEntity | undefined = await this.layoutRepository.findOne({ |
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.
Do we need to be explicit with the type? Is not infered from the method?
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, the findOne
return type is LayoutEntity
which is the issue I aim to solve with both PRs.
But in the second PR these types are removed ;)
@@ -32,6 +32,7 @@ import { CacheKeyPrefixEnum, InvalidateCacheService } from '../../../shared/serv | |||
import { SendMessageBase } from './send-message.base'; | |||
import { ApiException } from '../../../shared/exceptions/api.exception'; | |||
import { OrganizationEntity } from '../../../../../../../libs/dal/src/repositories/organization/organization.entity'; |
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 we don't. And this was probably and IDE playing around with a fix with an auto import. It is the only occurrence I could find in a quick search in the codebase.
@@ -1,5 +1,6 @@ | |||
// eslint-ignore max-len | |||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | |||
import { AnyKeys } from 'mongoose'; |
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.
Was going to highlight this. Also what's the difference between AnyKeys
type and Partial
the one implemented before?
|
||
export class BaseRepository<T_Query, T_Response> { | ||
public _model: Model<any & Document>; | ||
export class BaseRepository<T_DBModel, T_MappedEntity> { |
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 really like to differentiate the type of what the repository is expecting and what is returning. Was a pattern I used a lot in the past. I am happy you went that direction.
export class ChangeEntity { | ||
_id: string; | ||
|
||
_creatorId: string; | ||
|
||
_environmentId: string; | ||
_environmentId: EnvironmentId; |
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 am pending to implement the way that this really enforces the types to be of this kind (or flavour) as right now will still allow any string to be passed, so still open to get mixed.
export type ChangeDBModel = Omit< | ||
ChangeEntity, | ||
'_creatorId' | '_environmentId' | '_organizationId' | '_entityId' | '_parentId' | ||
> & { | ||
_creatorId: Types.ObjectId; | ||
|
||
_environmentId: Types.ObjectId; | ||
|
||
_organizationId: Types.ObjectId; | ||
|
||
_entityId: Types.ObjectId; | ||
|
||
_parentId?: Types.ObjectId; | ||
}; |
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.
Is this the replacement of the query enforcement that @djabarovgeorge implemented?
class PartialExecutionDetailsEntity extends Omit(ExecutionDetailsEntity, ['_environmentId', '_organizationId']) {} | ||
|
||
type EnforceEnvironmentQuery = FilterQuery<PartialExecutionDetailsEntity & Document> & | ||
({ _environmentId: string } | { _organizationId: string }); |
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 was implemented in purpose by @djabarovgeorge. @scopsy already flagged it but what's the replacement for this functionality?
.populate('template', selectTemplate) | ||
.populate('notification', selectNotification) | ||
.populate('subscriber', selectSubscriber) | ||
.populate('environment', selectEnvironment) | ||
.lean() | ||
.exec(); | ||
|
||
return job as unknown as JobEntityPopulated; |
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.
Try to replace this.MongooseModel
for this._model
in line 157 and see if that's the reason.
@@ -17,6 +17,8 @@ export class OrganizationEntity { | |||
partnerConfigurations?: IPartnerConfiguration[]; | |||
} | |||
|
|||
export type OrganizationDBModel = OrganizationEntity; |
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 find weird that the DBModel and the Entity are the same in this case. Shouldn't the entity return with the OrganizationId
for the _id
?
Also DbModel._id
should be of type Types.ObjectId
for coherence with the rest of the changes done.
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 DbModel._id
type is not required by the schema, it's created automatically, so the OrganizationEntity
just has to have it defined as the _id: string
as it does, and because of that the OrganizationDBModel = OrganizationEntity
for this case.
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 big work again.
For me is approved, but because the query enforcement might be sensitive I'd like to have full agreement with @scopsy and @djabarovgeorge. 👍🏻
@djabarovgeorge @scopsy @p-fernandez I've found the solution here for the env or org ids enforcement so it will stay ;) please check the latest commit |
Amazing! Looks great to me will review it again tomorrow 🙃, did you had the chance to test it locally it behaves as expected? |
yes, I've tested it and it works the exact same way ;) |
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.
🌟
So for future reference the use will be:
Mongoose -> FooEntity -> FooDBModel -> [MongoDB] -> FooDBModel -> FooEntity -> Mongoose?
Keeping all the transformations inside of the repositories?
import type { EnvironmentId } from '../repositories/environment'; | ||
import type { OrganizationId } from '../repositories/organization'; | ||
|
||
export type EnforceOrgId = { _organizationId: OrganizationId }; | ||
export type EnforceEnvOrOrgIds = { _environmentId: EnvironmentId } | EnforceOrgId; |
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! Only still missing thing (related to what I have pending) is that you could pass any string of any type and it will pass the validation, but for the enforcement query right now is enough. We can make it stricter in the future. 💪🏻
interface ISubscriberDocument extends SubscriberEntity, Document { | ||
_id: never; | ||
__v: never; | ||
} |
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.
👋🏻
export type TopicSubscribersDBModel = Omit< | ||
TopicSubscribersEntity, | ||
'_environmentId' | '_organizationId' | '_subscriberId' | '_topicId' | ||
> & { | ||
_environmentId: Types.ObjectId; | ||
|
||
_organizationId: Types.ObjectId; | ||
|
||
_subscriberId: Types.ObjectId; | ||
|
||
_topicId: Types.ObjectId; | ||
}; |
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.
Would be so cool if we could implement a generic that could do this automatic type (pseudocode):
type CustomOmit<T, <A extends ReadonlyArray<string>, V = Types.ObjectId>> = Omit<T, A> & {
[K in (A extends ReadonlyArray<infer A> ? A : never)]: V
}
So we could do something like this:
type TopicSubscriberDBModel = CustomOmit<TopicSubscribersEntity, <'_environmentId' | '_organizationId' | '_subscriberId' | '_topicId', Types.ObjectId>>;
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.
your wish is my order :D
type ChangePropsValueType<T, K extends keyof T, V = Types.ObjectId> = Omit<T, K> & {
[P in K]: V;
};
type TopicSubscribersDBModel = ChangePropsValueType<
TopicSubscribersEntity,
'_environmentId' | '_organizationId' | '_subscriberId' | '_topicId'
>;
exactly! |
What change does this PR introduce?
This is the first part of fixing the dal types. At the end of the second part we would have functions to return the correct types when using repository functions, for ex.
findById
will returnEntity | null
. Right now it's justEntity
which results in errors in the API app when we forget to add the null check.The Mongoose docs are suggesting using the
ObjectId
for the field that is "id", but we do type them withstring
type. Then our repositories are querying DB and doing a mapping to the entity type just to have strings as ids. But having the wrong types on the schema breaks all the Mongoose types system and model functions.In this PR I've introduced the
DBModel
interfaces that the Mongoose Schema uses and theEntity
type is returned back from the repositories like it was before, so nothing will have to be changed on the API app side.Why was this change needed?
Other information (Screenshots)