diff --git a/package.json b/package.json index dcb55a5..8611de6 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "husky": "^7.0.4", "i18next": "22.5.1", "i18next-browser-languagedetector": "^7.1.0", + "immer": "^10.0.3", "jest": "27.5.1", "js-logger": "^1.6.1", "lint-staged": "^12.3.7", diff --git a/src/general.js b/src/general.js index 5eea705..1e74cba 100644 --- a/src/general.js +++ b/src/general.js @@ -1,3 +1,5 @@ +import { produce } from "immer"; + import { complement, curry, @@ -13,7 +15,7 @@ import { * @param {T} func * @returns {T} */ -export const nullSafe = (func) => +export const nullSafe = func => // @ts-ignore curryN(func.length, (...args) => { const dataArg = args[func.length - 1]; @@ -23,7 +25,7 @@ export const nullSafe = (func) => export const noop = () => {}; -export const toLabelAndValue = (string) => ({ label: string, value: string }); +export const toLabelAndValue = string => ({ label: string, value: string }); // eslint-disable-next-line default-param-last export const getRandomInt = (a = Number.MAX_SAFE_INTEGER, b) => { @@ -57,3 +59,7 @@ export const isPresent = /*#__PURE__*/ complement(isNotPresent); export const notEqualsDeep = /*#__PURE__*/ complement(equals); export const isNotEqualDeep = notEqualsDeep; + +export const modifyWithImmer = /*#__PURE__*/ curry((modifier, data) => + produce(data, modifier) +); diff --git a/src/objects.js b/src/objects.js index 1fca9e4..c5fff3d 100644 --- a/src/objects.js +++ b/src/objects.js @@ -28,7 +28,7 @@ export const transformObjectDeep = ( } if (Array.isArray(object)) { - return object.map((obj) => + return object.map(obj => transformObjectDeep(obj, keyValueTransformer, objectPreProcessor) ); } else if (object === null || typeof object !== "object") { @@ -45,33 +45,29 @@ export const transformObjectDeep = ( ); }; -export const keysToCamelCase = (object) => +export const keysToCamelCase = object => transformObjectDeep(object, (key, value) => [snakeToCamelCase(key), value]); -export const keysToSnakeCase = (object) => +export const keysToSnakeCase = object => transformObjectDeep(object, (key, value) => [camelToSnakeCase(key), value]); -export const serializeKeysToSnakeCase = (object) => +export const serializeKeysToSnakeCase = object => transformObjectDeep( object, (key, value) => [camelToSnakeCase(key), value], - (object) => - typeof object?.toJSON === "function" ? object.toJSON() : object + object => (typeof object?.toJSON === "function" ? object.toJSON() : object) ); -export const preprocessForSerialization = (object) => +export const preprocessForSerialization = object => transformObjectDeep( object, (key, value) => [key, value], - (object) => - typeof object?.toJSON === "function" ? object.toJSON() : object + object => (typeof object?.toJSON === "function" ? object.toJSON() : object) ); -export const deepFreezeObject = (object) => { +export const deepFreezeObject = object => { if (object && typeof object === "object" && !Object.isFrozen(object)) { - Object.keys(object).forEach((property) => - deepFreezeObject(object[property]) - ); + Object.keys(object).forEach(property => deepFreezeObject(object[property])); Object.freeze(object); } @@ -82,7 +78,7 @@ export const matches = /*#__PURE__*/ curry((pattern, object) => matchesImpl(pattern, object) ); -export const filterNonNull = (object) => +export const filterNonNull = object => Object.fromEntries( Object.entries(object) .filter(([, v]) => !isNil(v)) diff --git a/tests/general.spec.js b/tests/general.spec.js index fb12b7f..d8d0d77 100644 --- a/tests/general.spec.js +++ b/tests/general.spec.js @@ -12,6 +12,7 @@ import { randomPick, toLabelAndValue, nullSafe, + modifyWithImmer, } from "src/general"; describe("General functions", () => { @@ -137,4 +138,21 @@ describe("General functions", () => { expect(isPresent(undefined)).toBe(false); expect(isPresent(null)).toBe(false); }); + + test("modifyWithImmer() should not mutate the original value", () => { + const state = [{ name: "Oliver" }, { name: "Eve" }]; + const nextState = modifyWithImmer(data => { + data[0].name = "Oliver smith"; + })(state); + expect(nextState).not.toBe(state); + }); + + test("modifyWithImmer() should work on objects", () => { + const state = { name: "Oliver" }; + expect( + modifyWithImmer(data => { + data.name = "Oliver smith"; + })(state) + ).toStrictEqual({ name: "Oliver smith" }); + }); }); diff --git a/typeTemplates/index.d.ts b/typeTemplates/index.d.ts index 6e92ce9..e22ae29 100644 --- a/typeTemplates/index.d.ts +++ b/typeTemplates/index.d.ts @@ -341,3 +341,12 @@ export function nullSafe( export function isNotPresent(object: any): boolean; export function isPresent(object: any): boolean; + +export function modifyWithImmer( + modifier: (draft: Draft) => void, + data: T +): T; + +export function modifyWithImmer( + modifier: (draft: Draft) => void +): (data: T) => T; diff --git a/yarn.lock b/yarn.lock index 9d8ef0f..cc0c74d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8701,6 +8701,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immer@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9" + integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A== + immutable@^4.0.0: version "4.2.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a"