4lessandrodev / rich-domain

A lib to help you create a robust project based on domain driven-design (ddd) principles with typescript and zero dependencies.
https://www.npmjs.com/package/rich-domain
MIT License
122 stars 5 forks source link

Types for (entity | valueObject) "toObject" method #145

Closed hikinine closed 6 months ago

hikinine commented 6 months ago

Saudações,

Entity.toObject method may not have much usability for production environments, but during development it is interesting to be able to check a serialized form of the Aggregate/Entity.

Proposal

Dynamically type the toObject function for the exact return of the autoMapper (primitive of all aggregates recursively).

Requirements

Include an method (or property) on base class (GettersAndSetters), OR, include a new method in each class. this method should return Props type

something like

class GettersAndSetters<Props> implements ... {
   ...
   getRawProps(): Props {
     return this.props
   } 
}

The reason for this is that you cannot infer a type that is protected/private (maybe I'm wrong).

Once we have the Entity Props available


type SerializerEntityReturnType<ThisEntity extends Entity<any>> = ReturnType<ThisEntity['getRaw']>
type SerializerValueObjectReturnType<ThisValueObject extends ValueObject<any>> = ReturnType<ThisValueObject['getRaw']>

export type Serializer<Props> = {
    [key in keyof Props]:
    Props[key] extends ValueObject<any>
    ? Serializer<SerializerValueObjectReturnType<Props[key]>>
    : Props[key] extends Entity<any>
    ? Serializer<SerializerEntityReturnType<Props[key]>>
    : Props[key] extends Array<any>
    ? Array<Serializer<ReturnType<Props[key][0]['getRaw']>>>
    : Props[key]
}

class Entity<Props> {
        //toObject within serializer
    toObject2(): Serializer<Props> {
        return this.autoMapper.entityToObj(this) as any;
    }
}

USAGE


interface NestedValueObjectProps {
    address: Address
    b: string
}
interface PostProps {
    title: Title // nested value object
    address: Address // nested value object
    multipleAddress: Address[] // nested array of value object

    singleComment: Comments // nested entity
    multipleComments: Comments[] // nested array of entities
    content: string
    whatever: number
    whatever2: boolean
    createdAt: Date
}
interface CommentsProps {
    fullName: FullName
    message: string
    createdAt: Date
}
class Address extends ValueObject<{
    street: string
    zip: string
    number: number
}> { }

class NestedValueObject extends ValueObject<NestedValueObjectProps> { }
class FullName extends ValueObject<string> { }
class Age extends ValueObject<number> { }
class Title extends ValueObject<string> { }

interface UserProps {
    fullName: FullName //works with ValueObject
    multiFullNames: FullName[] // works with arrays of ValueObjects

    age: Age
    multipleAges: Age[]

    nestedVo: NestedValueObject
    multipleNestedVo: NestedValueObject[]

    description: string // works with primary types
    createdAt: Date // works with objects

    post: Post
    multiplePost: Post[] // works with arrays of entities
}

class Comments extends Entity<CommentsProps> { }
class Post extends Entity<PostProps> { }
class User extends Entity<UserProps> { }

const user = new User({} as any)
const object = user.toObject2()
object.multiplePost[0].multipleAddress[0].number
4lessandrodev commented 6 months ago

hey @hikinine,

Thank you so much for opening this issue! Your suggestion for improving the toObject method is greatly appreciated. Indeed, enhancing it in this way has been on radar, especially considering its limited typing capabilities up to the first level and its compatibility only with TypeScript versions prior to 5.1.

toObject

It's worth noting that the current implementation of toObject may not fully meet your expectations due to its limitations. Specifically, it only provides typing up to the first level, and it's compatible only with TypeScript versions prior to 5.1.

Moreover, implementing this enhancement might require adjustments to the automapper functionality. Currently, automapper shortens objects that have only one attribute, which could affect the expected behavior of the method.

For instance, given the following example:


type Props = { value: string };

class Name extends ValueObject<Props> {
   public static init(name: string): Name {
      return new Name({ value: name });
   }
}

const name = Name.init("Jane");

console.log(name.toObject());
// Output: "Jane"

The output will only be the value, as automapper optimizes the object size by shortening the path.

Entityt Example:


type Props = { name: Name, age: Age };

class User extends Entity<Props> {
   public static init(name: Name, age: Age): User {
      return new User({ name, age });
   }
}

const user = User.init(name, age);

const model = user.toObject();
// Output: { "name": "Jane", "age": 21 }

As pointed out, the output is { "name": "Jane", "age": 21 } instead of { "name": { "value": "Jane" }, "age": { "value": 21 } }

Another important point

Ensuring that the toObject method returns read-only objects would be a significant enhancement, preventing unintended modifications to the state.

By returning read-only objects, we can provide an additional layer of protection against accidental data manipulation, while still allowing access to the object's properties for inspection purposes.

For example, consider the following scenario:


const company = Company.toObject();
company.users.push(user); // This should ideally result in a compilation error due to read-only nature

With read-only objects, attempts to modify the object's properties directly would be flagged at compile time, promoting safer coding practices and reducing the risk of runtime errors.

hikinine commented 6 months ago

Hi @4lessandrodev

As pointed out, the output is { "name": "Jane", "age": 21 } instead of { "name": { "value": "Jane" }, "age": { "value": 21 } }

Thats the goal, only the primitive object. I've tried on my own and looks like autoMapper returns exactly the same result as Serializer types.

Another important point Ensuring that the toObject method returns read-only objects would be a significant enhancement, preventing unintended modifications to the state.

Bem pensado, looks like autoMapper already creates a safe copy object, not referenced to the own aggregate props. But yes, should be nice turn everything on the Serializer result as readonly just for not miss understading.

Design Concerns

Something that is bothering me is the need to create a "getRawProps" method. The serializer does not necessarily need an implementation of the Props return. Just props inference at the typescript level. But I couldn't think of anything better than the method (which could be an antipattern since the intention is to protect properties and allow access only with the domain layer intelligence)

Anyway, let me know what you think.

Should I open this pull request?

4lessandrodev commented 6 months ago

@hikinine Absolutely, Feel free to go ahead and open it. We can then work on utilizing the existing toObject method to ensure that all type and value inference tests for the following scenarios pass:

Additionally, let's make sure to include an example of read-only implementation as follows:


    public toObject(): Readonly<Props> {
        return Object.freeze({ ... });
    }

Let's collaborate on this to ensure comprehensive testing and adherence to the desired functionality. Looking forward to working on this together!