Base solution for your next web application
Open Closed

Immutability With Nswag #10970


User avatar
0
cangunaydin created

Hello, I have a problem with immutability in angular project, I did some research about this and there are various ways to do this. But generated code from nswag is sometimes giving some problems. First of all as i see it there are 3 ways to do the immutability in plain javascript way.

// "Spread"
{ ...food }

const food = { beef: '🌽', bacon: '🥓' };

const newFood = {
  ...food,
  beef: '🥩',
};

// "Object.assign"
Object.assign({}, food)


let newCar=Object.assign(Object.create(myCar), {wheels: 3});


// "JSON"
JSON.parse(JSON.stringify(food))

third option with JSON is out of scope because it would be very difficult to copy the object with a changed property. and it is not most of the time very readable all around the project. and you loose methods in the class when you do the conversion.

Spread operator was my first approach to make the objects immutable, it is working fine until some point that i understand that spread operator spreads only the plain properties. This cause a problem if you have some methods on the class that you want to clone with the immutable object. One good example is Swagger generated DTOs. They have .toJSON() method in their classes if you use spread operator for those dtos then you can not send the dto to server side. Which gives an error.

Second approach works fine (of course if you want to shallow copy your object). If it is possible i want to make this generic and use it in every dto classes. https://mauricereigman.medium.com/immutable-typescript-classes-with-object-copywith-typesafe-a84fff3971dc I search for nswag configuration that i can inherit all dtos from the second approach sth like this

class ObjectCopier {
   public copyWith(modifyObject: { [P in keyof this]?: this[P] }): this {
      return Object.assign(Object.create(this.constructor.prototype), { ...this, ...modifyObject });
   }
}

but only thing i could do was to extend service classes. is there a way to extend all of the dtos in generated dtos from nswag?

Also i know that there are many other libraries out there. Some of them are Ramda,loadash-fp, immutable.js these are all fine and i can use them, but they mostly work with property path. So one good example can be found in loadash-fp

const myBooking = {
  id: '23484',
  client: {
      id: '458982',
      name: 'John Doe',
      email: '[email protected]',
      vip: false,
      business: false,
      family: false,
  }, 
  rooms: [
     {
       type: 'standard',
       beds: 2,       
     },
     {
       type: 'standard',
       beds: 1,       
     }
  ],
  extras: {
     lateCheckout: false,
     spa: false,
     minibar: false,
     seasights: false,
  }
}

const upgradeBooking = (propertypath, value, booking) => {
   return _.set(propertypath, value, booking);
}
const updatedBookingA = upgradeBooking('client.vip', true, myBooking);

so you are loosing the type-safe approach. If some change happens in dtos then 'client.vip' will not throw an error on compile time. there is another library that i have found out for this, called immutable-assign https://github.com/engineforce/ImmutableAssign but this library i believe use spread operator so you loose the methods for dtos.

another library that i have found out is monolite which works fine. This is the closest i can get until now. but this library is so small it is not updated regularly so i don't want to depend on this library https://github.com/kube/monolite

So as a conclusion, i am down to 2 or 3 options

  1. generate a base class for dtos that provides copyWith function
  2. if there is a way to make loadash-fp library to support typesafe predicate instead of property path. one example i have found with Ramda in this link. https://www.fullstackagile.eu/2020/07/31/typesafe-immutable-readable/
  3. keep using monolite

so the question here is what would be the best way to do, to clone the object with typesafe, immutable, readable and keeping the methods in tact inside the class?


4 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @cangunaydin

    We are not much experienced about this but option1 seems the easiest way to do this. It also keeps type-safetiy.

  • User Avatar
    0
    cangunaydin created

    Hello @ismcagdas. To do the option 1, I need to configure service.config.nswag file. Do you know how can i change the configuration? to inherit a custom base class?

    "codeGenerators": {
        "openApiToTypeScriptClient": {
          "className": "{controller}ServiceProxy",
          "moduleName": "",
          "namespace": "",
          "typeScriptVersion": 2.7,
          "template": "Angular",
          "promiseType": "Promise",
          "httpClass": "HttpClient",
          "useSingletonProvider": false,
          "injectionTokenType": "InjectionToken",
          "rxJsVersion": 6.0,
          "dateTimeType": "Luxon",
          "nullValue": "Undefined",
          "generateClientClasses": true,
          "generateClientInterfaces": false,
          "generateOptionalParameters": false,
          "exportTypes": true,
          "wrapDtoExceptions": false,
          "exceptionClass": "ApiException",
          "clientBaseClass": null,
          "wrapResponses": false,
          "wrapResponseMethods": [],
          "generateResponseClasses": true,
          "responseClass": "SwaggerResponse",
          "protectedMethods": [],
          "configurationClass": null,
          "useTransformOptionsMethod": false,
          "useTransformResultMethod": false,
          "generateDtoTypes": true,
          "operationGenerationMode": "MultipleClientsFromPathSegments",
          "markOptionalProperties": false,
          "generateCloneMethod": false,
          "typeStyle": "Class",
          "classTypes": [],
          "extendedClasses": [],
          "extensionCode": "service.extensions.ts",
          "generateDefaultValues": true,
          "excludedTypeNames": [],
          "excludedParameterNames": [],
          "handleReferences": false,
          "generateConstructorInterface": true,
          "convertConstructorInterfaceData": false,
          "importRequiredTypes": true,
          "useGetBaseUrlMethod": false,
          "baseUrlTokenName": "API_BASE_URL",
          "queryNullValue": "",
          "inlineNamedDictionaries": false,
          "inlineNamedAny": false,
          "templateDirectory": null,
          "typeNameGeneratorType": null,
          "propertyNameGeneratorType": null,
          "enumNameGeneratorType": null,
          "serviceHost": null,
          "serviceSchemes": null,
          "output": "../src/shared/service-proxies/service-proxies.ts"
        },
    

    if i give clientBaseClass: "ObjectCopier" and implement the class in "service.extensions.ts". It is creating it only on service classes. not on dtos, do you know a way to do it?

    class ObjectCopier {
       public copyWith(modifyObject: { [P in keyof this]?: this[P] }): this {
          return Object.assign(Object.create(this.constructor.prototype), { ...this, ...modifyObject });
       }
    }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @cangunaydin

    As I can see, NSWAG doesn't contain such an option. I also checked the issues on its GitHub repo but couldn't find a related issue. If you create an issue on NSWAG repo, I think they will evaluate your request.

  • User Avatar
    0
    cangunaydin created

    Hello @ismcagdas, I was working on this weekend and i come up with a solution, so if someone wants to implement this maybe it can be helpful who is reading this.

    First of all there is no direct option to extend the generated code from nswag option. However there is one trick you can do by using templates.

    1. You need to download the original template of dtos from this link. https://github.com/RicoSuter/NJsonSchema/tree/master/src/NJsonSchema.CodeGeneration.TypeScript/Templates I have only downloaded "Class.liquid" file since that is the only file i wanted to change.
    2. Then i have added my code after clone method.
    3. i configured my service.config.nswag file by pointing out to get the template files from my ext directory.

    that solved my problem. now i can use copyWith method for each dto that is generated.

    public copyWith(modifyObject: { [P in keyof this]?: this[P] }): this {
          return Object.assign(Object.create(this.constructor.prototype), { ...this, ...modifyObject });
       }
    

    and if someone is curious about it, i have also found a library that creates immutable objects in an efficient way by using proxy in angular. https://immerjs.github.io/immer/ it supports complex classes but you need to add [immerable] = true attribute to your object, so you can do the same trick if you want to use this library. https://immerjs.github.io/immer/complex-objects