dev-plusplus / coding-docs

4 stars 2 forks source link

DAO Pattern for Domain Objects in Typescript and Graphql #48

Open alacret opened 3 years ago

alacret commented 3 years ago

The purpose of this convention is to establish a new way of managing Domain Objects or Business Objects in the code. Currently, across the application, we use the Graphql types for managing data types of Business Objects. This approach has multiple problems:

Solution

The proposed solution involves the use of Classes instead of Type alias to manage Business Objects, for example, Users, Company, CompanyUser, Plan, etc

Taking into consideration the following aspects:

Additional considerations to implement this pattern:

Example:

type Role = {
  id:string
  name: string
};

type Profile {
  id:string;
  name: string;
  role: Role;
}

type UserType = {
  id: string;
  profile:Profile
  firstName?:string
}

// Data Object
class User{
  user:UserType
  constructor(user:UserType){
    this.user = user;
  }

  getRoleId = ()=> {
    if (this.user.profile === null)
      return null;
      if (this.user.profile.role === null)
        return null;
    return this.user.profile.role.id;
  }
}

const toObject<User, UserType> = (users:Array<UserType>) => {
  const userObjects = users.map(user:UserType => new User(user));
}

const action  = async () => {
  const result:Array<UserType> = await client.query();
  return toObject(result);
};

export default function App() {
  const users:Array<User> = await action();

  return (
    <div className="App">
      {users.map(user:User => user.getRoleId())}
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}
jesusrodrz commented 3 years ago

What you think of a generic class class that takes a object an exposed a method to read deep properties?


const user = {
  role: {
    id: 123,
    name: admin
  }
}

const daoUser = new DAO(user)

daoUser.getProperty('role.id') // the property path string could be mapped with typescript  without loosing the type safety 

We can use typescript literal types

alacret commented 3 years ago

What you think of a generic class class that takes a object an exposed a method to read deep properties?

const user = {
  role: {
    id: 123,
    name: admin
  }
}

const daoUser = new DAO(user)

daoUser.getProperty('role.id') // the property path string could be mapped with typescript  without loosing the type safety 

We can use typescript literal types

@jesusrodrz The purpose is to encapsulate how properties of the object are accessed. This encapsulation is not only for null checking or undefined, it can be for more complex evaluations, example:

class Company {
    ...
   getHeadCount = () => {
        if (this.company.offices === null)
              return 0;

        if (this.company.offices.lenght === 0)
              return 0;

        return this.company.offices.reduce(a,b => a + b);

   }
}
kikeztw commented 3 years ago

What you think of a generic class class that takes a object an exposed a method to read deep properties?

const user = {
  role: {
    id: 123,
    name: admin
  }
}

const daoUser = new DAO(user)

daoUser.getProperty('role.id') // the property path string could be mapped with typescript  without loosing the type safety 

We can use typescript literal types

this is daoUser.getProperty('role.id') not the same as doing this role.id The main thing here is that if later you need to make a change in the role.id object, you don't have to change it in each view

jesusrodrz commented 3 years ago

I've manage to create a function that takes input object and a object with getters functions and returns a object with the getters functions passed ad params


type Getters<Type> = {
  [Property in keyof Type]: () => Type[Property];
};
type InputGetters<T, Type> = {
  [Property in keyof Type]: (data: T) => Type[Property];
};

/**
 * @param object - Object Input.
 * @param getters - Getters functions.
 * @returns Mappen object.
 */
export function createDataAccessObject<
  U extends Exclude<Record<string, unknown>, 'set'>,
  T = unknown
>(
  object: T,
  getters: InputGetters<T, U>,
): Getters<U> & { set: (data: T) => void } {
  let state = object;

  const keys = Object.keys(getters) as (keyof U)[];

  const functions = keys.reduce(
    (prev, current) => ({
      ...prev,
      [current]: () => getters[current](state),
    }),
    {},
  ) as Getters<U>;

  return {
    ...functions,
    set: (data) => {
      state = data;
    },
  };
}

Implementation

const userGraphql = {
  role: {
    id: 123,
  },
  tenant: {
    id: 123,
    name: 'Main Tenant',
  },
};

const user = createDataAccessObject(userGraphql, {
  getRoleId: (data) => data.role.id,
  getTenantId: (data)=>data.tenant.id
});

user.getRoleId(); // returns role id

user.getTenantId(); // returns tenant id

we can't use classes because it doesn't allow dynamic methods

jesusrodrz commented 3 years ago

@alacret @kikeztw 👆🏽

kikeztw commented 3 years ago

The problem I see from this is that we cannot use it in the shared. The first parameter does not make much sense, we need to declare the methos that will be used to access the properties in the shared and then in the front or in the backend is that we will pass the data

I think the best thing is to use the classes since we can use decorator in the methods, we could create a couple of utils for certain methods of certain classes

  function first() {
    console.log("first(): factory evaluated");
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      console.log("first(): called");
    };
  }

  function second() {
    console.log("second(): factory evaluated");
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      console.log("second(): called");
    };
  }

  class ExampleClass {
    @first()
    @second()
    method() {}
  }
jesusrodrz commented 3 years ago

Refactoring the first approach we could manage to define the model in the shared project then use it on the frontend or cloud functions

type Getters<Type> = {
  [Property in keyof Type]: () => Type[Property];
};
type InputGetters<T, Type> = {
  [Property in keyof Type]: (data: T) => Type[Property];
};

/**
 * @param getters - Getters functions.
 * @returns Mappen object.
 */
export function createDataAccessObject<
  T,
  U extends Exclude<Record<string, unknown>, 'set'>
>(getters: InputGetters<T, U>): (data: T) => Getters<U> {
  return (state: T) => {
    const keys = Object.keys(getters) as (keyof U)[];

    const functions = keys.reduce(
      (prev, current) => ({
        ...prev,
        [current]: () => getters[current](state),
      }),
      {},
    ) as Getters<U>;

    return functions;
  };
}

Usage in share

type UserModel = {
  id: string;
  name: string;
  role: {
    id: string;
    name: string;
  };
  tenant: {
    id: string;
    name: string;
  };
};

type UserGetters = { getRoleId: string; getTenantId: string;}

export const getUser = createDataAccessObject<UserModel,UserGetters>({
  getRoleId: (data) => data.role.id,
  getTenantId: (data) => data.tenant.id,
});

Usage in front or cloud functions


import { getUser } from 'shared-package'

const userGraphql = {
  role: {
    id: 123,
  },
  tenant: {
    id: 123,
    name: 'Main Tenant',
  },
};

const user = getUser(userGraphql)

user.getRoleId(); // returns role id

user.getTenantId(); // returns tenant id
alacret commented 3 years ago

How this is better than the class approach?