diff --git a/.eslintrc.json b/.eslintrc.json index 1a12b9e06..d8a4f1137 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,7 +22,9 @@ "files": ["*.ts", "*.tsx"], "extends": ["plugin:@nrwl/nx/typescript"], "parserOptions": { "project": "./tsconfig.*?.json" }, - "rules": {} + "rules": { + + } }, { "files": ["*.js", "*.jsx"], diff --git a/jest.config.js b/jest.config.js index bcd104b08..879b80cea 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,8 @@ module.exports = { - projects: ['/packages/core', '/packages/classes'], + projects: [ + '/packages/core', + '/packages/classes', + '/packages/integration-test', + '/packages/pojos', + ], }; diff --git a/migrations.json b/migrations.json index 28ce4fff1..b33366952 100644 --- a/migrations.json +++ b/migrations.json @@ -1,18 +1,46 @@ { "migrations": [ { - "version": "10.4.5", - "description": "Update the 'update' npm script to invoke nx migrate", - "factory": "./src/migrations/update-10-4-0/update-script-to-invoke-nx-migrate", + "version": "11.0.0-beta.3", + "description": "Update the decoration script when using Angular CLI", + "factory": "./src/migrations/update-11-0-0/update-decorate-angular-cli", "package": "@nrwl/workspace", - "name": "update-script-to-invoke-nx-migrate" + "name": "update-decorate-angular-cli" }, { - "version": "10.4.6", - "description": "Update the decoration script when using Angular CLI", - "factory": "./src/migrations/update-10-4-0/update-decorate-angular-cli", + "version": "11.0.0-beta.3", + "description": "Update the @types/node package", + "factory": "./src/migrations/update-11-0-0/update-node-types", "package": "@nrwl/workspace", - "name": "update-decorate-angular-cli" + "name": "update-node-types" + }, + { + "version": "11.0.0-beta.3", + "description": "Rename tools/schematics into tools/generators", + "factory": "./src/migrations/update-11-0-0/rename-workspace-schematics", + "package": "@nrwl/workspace", + "name": "rename-workspace-schematics" + }, + { + "version": "11.0.0-beta.15", + "description": "Adds `outputs` based on builders", + "factory": "./src/migrations/update-11-0-0/add-outputs-in-workspace", + "package": "@nrwl/workspace", + "name": "add-outputs-in-workspace" + }, + { + "version": "11.0.0", + "description": "Check that the right update command is used", + "factory": "./src/migrations/update-11-0-0/update-command-check", + "package": "@nrwl/workspace", + "name": "update-command-check" + }, + { + "version": "11.0.2", + "description": "Rename the workspace-schematic script into workspace-generator script", + "factory": "./src/migrations/update-11-0-0/rename-workspace-schematic-script", + "package": "@nrwl/workspace", + "name": "rename-workspace-schematic-script" } ] } \ No newline at end of file diff --git a/nx.json b/nx.json index 3b5464a0f..3b9b34ae9 100644 --- a/nx.json +++ b/nx.json @@ -34,7 +34,13 @@ "classes": { "tags": [] }, - "models": { + "types": { + "tags": [] + }, + "integration-test": { + "tags": [] + }, + "pojos": { "tags": [] } }, diff --git a/package-lock.json b/package-lock.json index 7cda252ea..643f34a01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,48 +5,128 @@ "requires": true, "dependencies": { "@angular-devkit/architect": { - "version": "0.1001.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1001.7.tgz", - "integrity": "sha512-uFYIvMdewU44GbIyRfsUHNMLkx+C0kokpnj7eH5NbJfbyFpCfd3ijBHh+voPdPsDRWs9lLgjbxfHpswSPj4D8w==", + "version": "0.1100.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1100.5.tgz", + "integrity": "sha512-yOYfucNouc1doTbcGbCNMXGMSc36+j97XpdNoeGyzFQ7GwezLAro0a9gxc5PdOxndfelkND7J1JuOjxdW5O17A==", "dev": true, "requires": { - "@angular-devkit/core": "10.1.7", - "rxjs": "6.6.2" + "@angular-devkit/core": "11.0.5", + "rxjs": "6.6.3" + }, + "dependencies": { + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "@angular-devkit/build-webpack": { - "version": "0.1001.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1001.7.tgz", - "integrity": "sha512-2EUOkijukSVXJlMk5PTWlbAcTdMZ9dddRj4XoTLX1N/ZL5qBSE8BN+Jf5We/NPkAdq2apU8Crl9BuGcVeK4wOA==", + "version": "0.1100.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1100.5.tgz", + "integrity": "sha512-oD5t2oCfyiCyyeZckrqBnQco94zIMkRnRGzy3lFDH7KMiL0DG9l7x3nxn9H0YunYWr55LsGWwXGoR7l03Kl+jw==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1001.7", - "@angular-devkit/core": "10.1.7", - "rxjs": "6.6.2" + "@angular-devkit/architect": "0.1100.5", + "@angular-devkit/core": "11.0.5", + "rxjs": "6.6.3" + }, + "dependencies": { + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "@angular-devkit/core": { - "version": "10.1.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.7.tgz", - "integrity": "sha512-RRyDkN2FByA+nlnRx/MzUMK1FXwj7+SsrzJcvZfWx4yA5rfKmJiJryXQEzL44GL1aoaXSuvOYu3H72wxZADN8Q==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-11.0.5.tgz", + "integrity": "sha512-hwV8fjF8JNPJkiVWw8MNzeIfDo01aD/OAOlC4L5rQnVHn+i2EiU3brSDmFqyeHPPV3h/QjuBkS3tkN7gSnVWaQ==", "dev": true, "requires": { - "ajv": "6.12.4", + "ajv": "6.12.6", "fast-json-stable-stringify": "2.1.0", "magic-string": "0.25.7", - "rxjs": "6.6.2", + "rxjs": "6.6.3", "source-map": "0.7.3" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "@angular-devkit/schematics": { - "version": "10.1.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-10.1.7.tgz", - "integrity": "sha512-nk9RXA09b+7uq59HS/gyztNzUGHH/eQAUQhWHdDYSCG6v1lhJVCKx1HgDPELVxmeq9f+HArkAW7Y7c+ccdNQ7A==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-11.0.5.tgz", + "integrity": "sha512-0NKGC8Nf/4vvDpWKB7bwxIazvNnNHnZBX6XlyBXNl+fW8tpTef3PNMJMSErTz9LFnuv61vsKbc36u/Ek2YChWg==", "dev": true, "requires": { - "@angular-devkit/core": "10.1.7", - "ora": "5.0.0", - "rxjs": "6.6.2" + "@angular-devkit/core": "11.0.5", + "ora": "5.1.0", + "rxjs": "6.6.3" + }, + "dependencies": { + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "@babel/code-frame": { @@ -967,92 +1047,51 @@ } }, "@nestjs/schematics": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-7.2.5.tgz", - "integrity": "sha512-RwoPNV6ot1sCq+/CDH9fDAE/NX/r6UtzDGn+S/XiEIP4AuRqUphKPyAMxXc8k4dLi3VFRCchAshYQfaOgDc9Qg==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-7.2.6.tgz", + "integrity": "sha512-4geGO9pjYG4Sc4Qi+pkUVIbaxPEeySHi/z17po8nP9uaPPo8AUKP9rXjNL+mhMrXqFlB/hhN6xBBYtMyL5pB2Q==", "dev": true, "requires": { - "@angular-devkit/core": "11.0.1", - "@angular-devkit/schematics": "11.0.1", + "@angular-devkit/core": "11.0.5", + "@angular-devkit/schematics": "11.0.5", "fs-extra": "9.0.1", "pluralize": "8.0.0" }, "dependencies": { - "@angular-devkit/core": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-11.0.1.tgz", - "integrity": "sha512-ui3g7w/0SpU9oq8uwN9upR8Y1eOXZ+P2p3NyDydBrR7ZEfEkRLS1mhozN/ib8farrwK5N3kIIJxMb5t3187Hng==", - "dev": true, - "requires": { - "ajv": "6.12.6", - "fast-json-stable-stringify": "2.1.0", - "magic-string": "0.25.7", - "rxjs": "6.6.3", - "source-map": "0.7.3" - } - }, - "@angular-devkit/schematics": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-11.0.1.tgz", - "integrity": "sha512-rAOnAndcybEH398xf5wzmcUPCoCi0dKiOo/+1dkKU5aTxynw1OUnANt5K6A+ZZTGnJmfjtP0ovkZGYun9IUDxQ==", - "dev": true, - "requires": { - "@angular-devkit/core": "11.0.1", - "ora": "5.1.0", - "rxjs": "6.6.3" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "ora": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.1.0.tgz", - "integrity": "sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w==", + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "dev": true, "requires": { - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.4.0", - "is-interactive": "^1.0.0", - "log-symbols": "^4.0.0", - "mute-stream": "0.0.8", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" } }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "requires": { - "tslib": "^1.9.0" + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true } } @@ -1090,59 +1129,79 @@ "dev": true, "requires": { "mkdirp": "^1.0.4" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } } }, "@nrwl/cli": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/cli/-/cli-10.4.7.tgz", - "integrity": "sha512-Vr0a3WsQhyRzp5yA4mYxEito9OBbkN4H+dkzgZc0THm9xyVvTWceEtaz81VwPNyrfiqyRfvptPPDgNtKhhvtRA==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/cli/-/cli-11.0.20.tgz", + "integrity": "sha512-ss4eHruXeFWtbTUAV0AjwOLJHi/p23QN9LXTrX45wkT0qM+B1ysL/is3V1pItHkSNJr6Ab/m00lATGORqi0b7Q==", "dev": true, "requires": { - "@nrwl/tao": "10.4.7", - "chalk": "2.4.2", + "@nrwl/tao": "11.0.20", + "chalk": "4.1.0", "tmp": "0.0.33", "yargs": "15.4.1", "yargs-parser": "20.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@nrwl/devkit": { + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-11.0.20.tgz", + "integrity": "sha512-enhdAZeC96ZQad9lciNtm7CGNjI8PUNme4J5/y2ZlGMShrqDaijag9yktVVkErfdizaXL4o2Rny7UFXSXdElEw==", + "dev": true, + "requires": { + "@nrwl/tao": "11.0.20", + "ejs": "^3.1.5", + "strip-json-comments": "2.0.1", + "tslib": "^2.0.0" } }, "@nrwl/eslint-plugin-nx": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-10.4.7.tgz", - "integrity": "sha512-2Ty6/g0auxW05WyFf/gRI1xzBMtI5hyZHOYULxp9zSPsh7zx2eH/d58S4KFFzPpliLC3LsiP9K4AtPEYBx5clQ==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-11.0.20.tgz", + "integrity": "sha512-A7EZTJQE8h1OZ15dxp8bPZkGDfTCuHoSlT2nGjPDM3rDfks4cI8bzlfWHQq8kYXAlLLCOs+tBb2+MNchX7rMGQ==", "dev": true, "requires": { - "@angular-devkit/core": "~10.1.3", + "@angular-devkit/core": "~11.0.1", "@typescript-eslint/experimental-utils": "^4.3.0", "confusing-browser-globals": "^1.0.9" } }, "@nrwl/jest": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/jest/-/jest-10.4.7.tgz", - "integrity": "sha512-n0ZfTmRKIbOOmNOd1FDeW5SN9ZfWYkUBB2ouQm+OtI45P5uava7d2OptZ1jRQCQbB3Y6HWvS1xvkZWxcrlSWBQ==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/jest/-/jest-11.0.20.tgz", + "integrity": "sha512-K2a/tOLnYrLFZtbUhoMD51DZTPp+qfjYKBAd5hOCgErx1+Asw+LlIqEXxOZ3nj6vBBphH1OONEp9QCoVZKRA8w==", "dev": true, "requires": { - "@angular-devkit/architect": "~0.1001.3", - "@angular-devkit/core": "~10.1.3", - "@angular-devkit/schematics": "~10.1.3", - "rxjs": "^6.5.4" + "@angular-devkit/architect": "~0.1100.1", + "@angular-devkit/core": "~11.0.1", + "@angular-devkit/schematics": "~11.0.1", + "@nrwl/devkit": "11.0.20", + "jest-resolve": "^26.6.2", + "rxjs": "^6.5.4", + "strip-json-comments": "2.0.1", + "tslib": "^2.0.0" } }, "@nrwl/linter": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/linter/-/linter-10.4.7.tgz", - "integrity": "sha512-Om1zTLYSOum6oUkwPZTcmdw24UHFuef+kH22PaH7lDjeRwYbThmDoxaKB5oPh+rrrysaeXYrEilhYT8e/9cAng==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/linter/-/linter-11.0.20.tgz", + "integrity": "sha512-euIIT36EuwIu2baNGK+xB4rXxpluvADtMWbY8iv5hkjdHgK0tvx/rNuHPxrrtiDxGQCAijqrzkR3PggtIJrbeg==", "dev": true, "requires": { - "@angular-devkit/architect": "~0.1001.3", + "@angular-devkit/architect": "~0.1100.1", "glob": "7.1.4", "minimatch": "3.0.4", "tslib": "^2.0.0" @@ -1165,33 +1224,39 @@ } }, "@nrwl/nest": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/nest/-/nest-10.4.7.tgz", - "integrity": "sha512-Vfb+2CrYU4NSP/iPh/A1sfkwQ3q3CUBiPTmCdBWHmM2rpj8iVLPHU+5+UL3DSMxY52rNYrVUI2M5fJEh9UkDHQ==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/nest/-/nest-11.0.20.tgz", + "integrity": "sha512-aJuSkIpat3HEO1zw/T92+OCI6eJTcLvHwS9ogYvsHj+MboTAKxXYDi6Kgm+R9G35n56chY67tPb/FIN8mFcdeA==", "dev": true, "requires": { - "@angular-devkit/schematics": "~10.1.3", + "@angular-devkit/core": "~11.0.1", + "@angular-devkit/schematics": "~11.0.1", "@nestjs/schematics": "^7.0.0", - "@nrwl/jest": "10.4.7", - "@nrwl/node": "10.4.7" + "@nrwl/devkit": "11.0.20", + "@nrwl/jest": "11.0.20", + "@nrwl/node": "11.0.20" } }, "@nrwl/node": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/node/-/node-10.4.7.tgz", - "integrity": "sha512-0w/ZyC3NbJczahoAKtf2LzXlIZbaO6Bqn0Co33mY13swSJGbMyXjineGL8XHktDLm3HMVui1Ok7yyp2oHQOplw==", - "dev": true, - "requires": { - "@angular-devkit/architect": "~0.1001.3", - "@angular-devkit/build-webpack": "~0.1001.3", - "@angular-devkit/core": "~10.1.3", - "@angular-devkit/schematics": "~10.1.3", - "@nrwl/jest": "10.4.7", - "@nrwl/linter": "10.4.7", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/node/-/node-11.0.20.tgz", + "integrity": "sha512-abUYhn+AYXYhW2Is51e1cgwjDX7pU53UT0quAMvYLjemPjle0SMIStD5+ibQip+gJ9lZqpgo8wm307rOQSNO3g==", + "dev": true, + "requires": { + "@angular-devkit/architect": "~0.1100.1", + "@angular-devkit/build-webpack": "~0.1100.1", + "@angular-devkit/core": "~11.0.1", + "@angular-devkit/schematics": "~11.0.1", + "@nrwl/devkit": "11.0.20", + "@nrwl/jest": "11.0.20", + "@nrwl/linter": "11.0.20", "circular-dependency-plugin": "5.2.0", "copy-webpack-plugin": "6.0.3", "fork-ts-checker-webpack-plugin": "^3.1.1", + "fs-extra": "7.0.1", + "glob": "7.1.4", "license-webpack-plugin": "2.1.2", + "rxjs": "^6.5.4", "source-map-support": "0.5.16", "tree-kill": "1.2.2", "ts-loader": "5.4.5", @@ -1203,6 +1268,20 @@ "webpack-node-externals": "1.7.2" }, "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1222,41 +1301,58 @@ } }, "@nrwl/tao": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-10.4.7.tgz", - "integrity": "sha512-uTTgbf77ai6NXz8Y9E1nilRjCOB7qOqTaxNIEcymHBRkxoiPQYjWIiVAyL45OecFvPW3Mk2ZnpRyaw+7uA2+oA==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-11.0.20.tgz", + "integrity": "sha512-S/jaeenZ0ueQZgz54AGhMtK1Obs6KTwsojfHdB+FiEve2ipBbKKeBZXXfeXlH2priH5ej5FhosCEV1DKqXLlUg==", "dev": true, "requires": { - "@angular-devkit/architect": "~0.1001.3", - "@angular-devkit/core": "~10.1.3", - "@angular-devkit/schematics": "~10.1.3", + "@angular-devkit/architect": "~0.1100.1", + "@angular-devkit/core": "~11.0.1", + "@angular-devkit/schematics": "~11.0.1", + "chalk": "4.1.0", + "fs-extra": "7.0.1", "inquirer": "^6.3.1", - "minimist": "^1.2.0", + "minimist": "^1.2.5", + "rxjs": "^6.5.4", "semver": "6.3.0", "strip-json-comments": "2.0.1", "tmp": "0.0.33", "tslib": "^2.0.0", "yargs-parser": "20.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "@nrwl/workspace": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/@nrwl/workspace/-/workspace-10.4.7.tgz", - "integrity": "sha512-L8UTA4Z5ItCNMrptaXicC7zn9qcNrq02NhQ92YAh3KBABxwHMkCvyISxRaXG6jAjNfslP4U//MezcGG++G1AOA==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nrwl/workspace/-/workspace-11.0.20.tgz", + "integrity": "sha512-tpT7mgKkLNh2ZCcmseyflcyxAA96/FCZHnPFrl65n79GVdy/ScyZ/cVArTR8QYloICg++41SB+jfEtJiOxMyeA==", "dev": true, "requires": { - "@angular-devkit/architect": "~0.1001.3", - "@angular-devkit/core": "~10.1.3", - "@angular-devkit/schematics": "~10.1.3", - "@nrwl/cli": "10.4.7", + "@angular-devkit/architect": "~0.1100.1", + "@angular-devkit/core": "~11.0.1", + "@angular-devkit/schematics": "~11.0.1", + "@nrwl/cli": "11.0.20", + "@nrwl/devkit": "11.0.20", "axios": "0.19.2", - "chalk": "2.4.2", + "chalk": "4.1.0", "cosmiconfig": "^4.0.0", "dotenv": "6.2.0", "flat": "^5.0.2", "fs-extra": "7.0.1", "ignore": "^5.0.4", "inquirer": "^6.3.1", + "lodash": "^4.17.20", "minimatch": "3.0.4", "npm-run-all": "^4.1.5", "opn": "^5.3.0", @@ -1270,22 +1366,21 @@ "yargs-parser": "20.0.0" }, "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, "dotenv": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", "dev": true - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } } } }, @@ -1420,9 +1515,9 @@ "dev": true }, "@types/node": { - "version": "14.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.12.tgz", - "integrity": "sha512-ASH8OPHMNlkdjrEdmoILmzFfsJICvhBsFfAum4aKZ/9U4B6M6tTmTPh+f3ttWdD74CEGV5XvXWkbyfSdXaTd7g==", + "version": "12.12.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.38.tgz", + "integrity": "sha512-75eLjX0pFuTcUXnnWmALMzzkYorjND0ezNEycaKesbUBg9eGZp4GHPuDmkRc4mQQvIpe29zrzATNRA6hkYqwmA==", "dev": true }, "@types/normalize-package-data": { @@ -1491,13 +1586,13 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.9.1.tgz", - "integrity": "sha512-QRLDSvIPeI1pz5tVuurD+cStNR4sle4avtHhxA+2uyixWGFjKzJ+EaFVRW6dA/jOgjV5DTAjOxboQkRDE8cRlQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.11.1.tgz", + "integrity": "sha512-fABclAX2QIEDmTMk6Yd7Muv1CzFLwWM4505nETzRHpP3br6jfahD9UUJkhnJ/g2m7lwfz8IlswcwGGPGiq9exw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.9.1", - "@typescript-eslint/scope-manager": "4.9.1", + "@typescript-eslint/experimental-utils": "4.11.1", + "@typescript-eslint/scope-manager": "4.11.1", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -1505,6 +1600,62 @@ "tsutils": "^3.17.1" }, "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.11.1.tgz", + "integrity": "sha512-mAlWowT4A6h0TC9F+J5pdbEhjNiEMO+kqPKQ4sc3fVieKL71dEqfkKgtcFVSX3cjSBwYwhImaQ/mXQF0oaI38g==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.11.1", + "@typescript-eslint/types": "4.11.1", + "@typescript-eslint/typescript-estree": "4.11.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.11.1.tgz", + "integrity": "sha512-Al2P394dx+kXCl61fhrrZ1FTI7qsRDIUiVSuN6rTwss6lUn8uVO2+nnF4AvO0ug8vMsy3ShkbxLu/uWZdTtJMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.1", + "@typescript-eslint/visitor-keys": "4.11.1" + } + }, + "@typescript-eslint/types": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.11.1.tgz", + "integrity": "sha512-5kvd38wZpqGY4yP/6W3qhYX6Hz0NwUbijVsX2rxczpY6OXaMxh0+5E5uLJKVFwaBM7PJe1wnMym85NfKYIh6CA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.1.tgz", + "integrity": "sha512-tC7MKZIMRTYxQhrVAFoJq/DlRwv1bnqA4/S2r3+HuHibqvbrPcyf858lNzU7bFmy4mLeIHFYr34ar/1KumwyRw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.1", + "@typescript-eslint/visitor-keys": "4.11.1", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.1.tgz", + "integrity": "sha512-IrlBhD9bm4bdYcS8xpWarazkKXlE7iYb1HzRuyBP114mIaj5DJPo11Us1HgH60dTt41TCZXMaTCAW+OILIYPOg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.1", + "eslint-visitor-keys": "^2.0.0" + } + }, "semver": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", @@ -1546,15 +1697,83 @@ } }, "@typescript-eslint/parser": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.9.1.tgz", - "integrity": "sha512-Gv2VpqiomvQ2v4UL+dXlQcZ8zCX4eTkoIW+1aGVWT6yTO+6jbxsw7yQl2z2pPl/4B9qa5JXeIbhJpONKjXIy3g==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.11.1.tgz", + "integrity": "sha512-BJ3jwPQu1jeynJ5BrjLuGfK/UJu6uwHxJ/di7sanqmUmxzmyIcd3vz58PMR7wpi8k3iWq2Q11KMYgZbUpRoIPw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.9.1", - "@typescript-eslint/types": "4.9.1", - "@typescript-eslint/typescript-estree": "4.9.1", + "@typescript-eslint/scope-manager": "4.11.1", + "@typescript-eslint/types": "4.11.1", + "@typescript-eslint/typescript-estree": "4.11.1", "debug": "^4.1.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.11.1.tgz", + "integrity": "sha512-Al2P394dx+kXCl61fhrrZ1FTI7qsRDIUiVSuN6rTwss6lUn8uVO2+nnF4AvO0ug8vMsy3ShkbxLu/uWZdTtJMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.1", + "@typescript-eslint/visitor-keys": "4.11.1" + } + }, + "@typescript-eslint/types": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.11.1.tgz", + "integrity": "sha512-5kvd38wZpqGY4yP/6W3qhYX6Hz0NwUbijVsX2rxczpY6OXaMxh0+5E5uLJKVFwaBM7PJe1wnMym85NfKYIh6CA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.1.tgz", + "integrity": "sha512-tC7MKZIMRTYxQhrVAFoJq/DlRwv1bnqA4/S2r3+HuHibqvbrPcyf858lNzU7bFmy4mLeIHFYr34ar/1KumwyRw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.1", + "@typescript-eslint/visitor-keys": "4.11.1", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.1.tgz", + "integrity": "sha512-IrlBhD9bm4bdYcS8xpWarazkKXlE7iYb1HzRuyBP114mIaj5DJPo11Us1HgH60dTt41TCZXMaTCAW+OILIYPOg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.1", + "eslint-visitor-keys": "^2.0.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } } }, "@typescript-eslint/scope-manager": { @@ -2066,19 +2285,16 @@ "dev": true }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true }, "async-each": { "version": "1.0.3", @@ -2642,12 +2858,6 @@ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -2679,27 +2889,10 @@ "mkdirp": "^1.0.3", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.0", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" } }, "cache-base": { @@ -3292,6 +3485,26 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "copy-descriptor": { @@ -3689,6 +3902,15 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } } } }, @@ -3883,6 +4105,15 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-dldq3ZfFtgVTJMLjOe+/3sROTzALlL9E34V4/sDtUd/KlBSS0s6U1/+WPE1B4sj9CXHJpL1M6rhNJnc9Wbal9w==", + "dev": true, + "requires": { + "jake": "^10.6.1" + } + }, "elliptic": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", @@ -3960,9 +4191,9 @@ } }, "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, "requires": { "prr": "~1.0.1" @@ -4081,9 +4312,9 @@ } }, "eslint": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.15.0.tgz", - "integrity": "sha512-Vr64xFDT8w30wFll643e7cGrIkPEU50yIiI36OdSIDoSGguIeaLzBo0vpGvzo9RECUqq7htURfwEtKqwytkqzA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.16.0.tgz", + "integrity": "sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -4120,7 +4351,7 @@ "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^5.2.3", + "table": "^6.0.4", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -4200,9 +4431,9 @@ } }, "eslint-config-prettier": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz", - "integrity": "sha512-8Y8lGLVPPZdaNA7JXqnvETVC7IiVRgAP6afQu9gOQRn90YY3otMNh+x7Vr2vMePQntF+5erdSUBqSzCmU/AxaQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz", + "integrity": "sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA==", "dev": true }, "eslint-scope": { @@ -4745,6 +4976,15 @@ "dev": true, "optional": true }, + "filelist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", + "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -4857,17 +5097,6 @@ "requires": { "flatted": "^3.1.0", "rimraf": "^3.0.2" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } } }, "flatted": { @@ -4887,9 +5116,9 @@ } }, "follow-redirects": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", - "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", "dev": true }, "for-in": { @@ -4971,41 +5200,14 @@ } }, "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - }, - "dependencies": { - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", - "dev": true - } + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, "fs-minipass": { @@ -5067,9 +5269,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", + "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", "dev": true, "requires": { "function-bind": "^1.1.1", @@ -5374,9 +5576,9 @@ } }, "html-entities": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", - "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", "dev": true }, "html-escaper": { @@ -6068,6 +6270,18 @@ "istanbul-lib-report": "^3.0.0" } }, + "jake": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", + "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "dev": true, + "requires": { + "async": "0.9.x", + "chalk": "^2.4.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + } + }, "jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", @@ -7596,13 +7810,10 @@ } }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true }, "move-concurrently": { "version": "1.0.1", @@ -7616,6 +7827,26 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "ms": { @@ -8049,9 +8280,9 @@ } }, "ora": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.0.0.tgz", - "integrity": "sha512-s26qdWqke2kjN/wC4dy+IQPBIMWBJlSU/0JZhk30ZDBLelW25rv66yutUWARMigpGPzcXHb+Nac5pNhN/WsARw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.1.0.tgz", + "integrity": "sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -8376,6 +8607,15 @@ "mkdirp": "^0.5.5" }, "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -8385,6 +8625,15 @@ "ms": "^2.1.1" } }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8703,6 +8952,12 @@ "picomatch": "^2.2.1" } }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -8936,9 +9191,9 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -9313,38 +9568,20 @@ "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true } } @@ -9945,47 +10182,32 @@ "dev": true }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.4.tgz", + "integrity": "sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^6.12.4", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } } } @@ -10008,14 +10230,6 @@ "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } } }, "terminal-link": { @@ -10159,6 +10373,15 @@ "semver": "^5.6.0" } }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -10183,6 +10406,15 @@ "find-up": "^3.0.0" } }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -10509,64 +10741,6 @@ "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", "dev": true }, - "tslint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.0.0.tgz", - "integrity": "sha512-9nLya8GBtlFmmFMW7oXXwoXS1NkrccqTqAtwXzdPV9e2mqSEvCki6iHL/Fbzi5oqbugshzgGPk7KBb2qNP1DSA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.10.0", - "tsutils": "^2.29.0" - }, - "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -11121,6 +11295,15 @@ "readable-stream": "^2.0.1" } }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -11135,9 +11318,9 @@ } }, "webpack-dev-middleware": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", - "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", "dev": true, "requires": { "memory-fs": "^0.4.1", @@ -11158,10 +11341,19 @@ } }, "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", + "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==", "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } } } }, diff --git a/package.json b/package.json index dfb6c5532..a5526f669 100644 --- a/package.json +++ b/package.json @@ -21,35 +21,35 @@ "format:write": "nx format:write", "format:check": "nx format:check", "update": "nx migrate latest", - "workspace-schematic": "nx workspace-schematic", "dep-graph": "nx dep-graph", "help": "nx help", - "commit": "cz" + "commit": "cz", + "workspace-generator": "nx workspace-generator" }, "private": true, "dependencies": {}, "devDependencies": { - "@nrwl/cli": "10.4.7", - "@nrwl/eslint-plugin-nx": "10.4.7", - "@nrwl/jest": "10.4.7", - "@nrwl/nest": "10.4.7", - "@nrwl/node": "10.4.7", - "@nrwl/tao": "10.4.7", - "@nrwl/workspace": "10.4.7", + "@nrwl/cli": "11.0.20", + "@nrwl/eslint-plugin-nx": "11.0.20", + "@nrwl/jest": "11.0.20", + "@nrwl/nest": "11.0.20", + "@nrwl/node": "11.0.20", + "@nrwl/tao": "11.0.20", + "@nrwl/workspace": "11.0.20", "@types/jest": "26.0.19", - "@types/node": "14.14.12", - "@typescript-eslint/eslint-plugin": "4.9.1", - "@typescript-eslint/parser": "4.9.1", + "@types/node": "12.12.38", + "@typescript-eslint/eslint-plugin": "4.11.1", + "@typescript-eslint/parser": "4.11.1", "commitizen": "4.2.2", "cz-conventional-changelog": "3.3.0", "dotenv": "8.2.0", - "eslint": "7.15.0", - "eslint-config-prettier": "7.0.0", + "eslint": "7.16.0", + "eslint-config-prettier": "7.1.0", "jest": "26.6.3", "prettier": "2.2.1", + "reflect-metadata": "^0.1.13", "ts-jest": "26.4.4", "ts-node": "9.1.1", - "tslint": "6.0.0", "typescript": "4.0.5" }, "config": { diff --git a/packages/classes/.eslintrc.json b/packages/classes/.eslintrc.json index 54ac39535..f2c2a56a3 100644 --- a/packages/classes/.eslintrc.json +++ b/packages/classes/.eslintrc.json @@ -1 +1,10 @@ -{ "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } +{ + "extends": "../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "rules": { + "@typescript-eslint/no-non-null-assertion": 0, + "no-prototype-builtins": 0 + } +} diff --git a/packages/classes/jest.config.js b/packages/classes/jest.config.js index 6b14fcb79..4151d7ba6 100644 --- a/packages/classes/jest.config.js +++ b/packages/classes/jest.config.js @@ -3,7 +3,7 @@ module.exports = { preset: '../../jest.preset.js', globals: { 'ts-jest': { - tsConfig: '/tsconfig.spec.json', + tsconfig: '/tsconfig.spec.json', }, }, testEnvironment: 'node', diff --git a/packages/classes/package.json b/packages/classes/package.json index ae1a757db..49321925e 100644 --- a/packages/classes/package.json +++ b/packages/classes/package.json @@ -1,4 +1,7 @@ { "name": "@automapper/classes", - "version": "0.0.1" + "version": "0.0.1", + "peerDependencies": { + "reflect-metadata": "~0.1.13" + } } diff --git a/packages/classes/src/index.ts b/packages/classes/src/index.ts index ab7c23e4c..691533134 100644 --- a/packages/classes/src/index.ts +++ b/packages/classes/src/index.ts @@ -1 +1,2 @@ export * from './lib/classes'; +export * from './lib/decorators'; diff --git a/packages/classes/src/lib/classes.spec.ts b/packages/classes/src/lib/classes.spec.ts deleted file mode 100644 index 9f020288a..000000000 --- a/packages/classes/src/lib/classes.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('classes', () => { - it('should work', () => { - expect(true).toEqual(true); - }); -}); diff --git a/packages/classes/src/lib/classes.ts b/packages/classes/src/lib/classes.ts index c10169f80..0a6dcd0ac 100644 --- a/packages/classes/src/lib/classes.ts +++ b/packages/classes/src/lib/classes.ts @@ -1,5 +1,218 @@ -function classes(): string { - return 'classes'; +import { createInitialMapping } from '@automapper/core'; +import type { + Dictionary, + ErrorHandler, + Mapping, + MapPlugin, +} from '@automapper/types'; +import { MappingClassId } from '@automapper/types'; +import 'reflect-metadata'; +import { + ClassInstanceStorage, + ClassMappingStorage, + ClassMetadataStorage, +} from './storages'; +import type { Constructible } from './types'; +import { instantiate, isClass } from './utils'; + +/** + * + * A MapPlugin to work with JS/TS Classes. + * + * @param {ErrorHandler} errorHandler + */ +export const classes = ( + errorHandler: ErrorHandler +): MapPlugin => { + // Initialize all the storages + const metadataStorage = new ClassMetadataStorage(); + const mappingStorage = new ClassMappingStorage(); + const instanceStorage = new ClassInstanceStorage(); + + return { + initializeMapping(source, destination, options?) { + // If a mapping already exists, handle error and return; + if (mappingStorage.has(source, destination)) { + errorHandler.handle( + `Mapping for source ${source.toString()} and destination ${destination.toString()} already exists` + ); + return; + } + + // Run the source and destination through Reflection to update storages + // with information/metadata about source and destination + exploreMetadata(metadataStorage, instanceStorage, source, destination); + + /** + * Instantiate a new instance of Destination/Source along with any nested constructible + * + * ```ts + * Foo { + * bar: Bar; + * } + * ``` + * `Foo#bar` is a nested constructible + */ + const [destinationInstance, destinationNestedConstructible] = instantiate( + instanceStorage, + metadataStorage, + destination + ); + + const [sourceInstance, sourceNestedConstructible] = instantiate( + instanceStorage, + metadataStorage, + source + ); + + // Get a hold of the prototype of Source (in case of inheritance with extends keyword) + const sourceProto = Object.getPrototypeOf(source); + + // Call `createInitialMapping` from the core package + return createInitialMapping( + sourceInstance, + destinationInstance, + sourceNestedConstructible, + destinationNestedConstructible, + (mapping) => { + mappingStorage.set(source, destination, mapping); + }, + options, + { + // classes plugin needs to pre-process the prototype of Source + // before looping through the properties on the Destination + prePropertiesLoop: prePropertiesLoop( + source, + metadataStorage, + instanceStorage, + sourceInstance, + sourceNestedConstructible + ), + // classes plugin needs to check for sourcePaths on the prototype of Source + isMultipartSourcePathsInSource, + // classes plugin needs to check for the destinationPath (sourcePath) on the prototype of Source + isDestinationPathOnSource: isDestinationPathOnSource(sourceProto), + } + ); + }, + getMapping(source, destination) { + // get mapping of source and destination from mappingStorage + const mapping = mappingStorage.get(source, destination); + + // handle error and fail fast if not found + if (!mapping) { + errorHandler.handle( + `Mapping not found for source ${source.toString()} and destination ${destination.toString()}` + ); + return; + } + + // run preMap to get new instances of source and destination for mapping[MappingClassId.mappings] + // this is to prevent mutation + mapping[MappingClassId.mappings] = this.preMap(source, destination); + + // return the mapping + return mapping; + }, + preMap< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + source, + destination, + sourceObj: TSource = undefined, + destinationObj: TDestination = undefined + ) { + // Prepare the sourceInstance/destinationInstance with plain object sourceObj and destinationObj + const [sourceInstance] = instantiate( + instanceStorage, + metadataStorage, + source, + sourceObj + ); + const [destinationInstance] = instantiate( + instanceStorage, + metadataStorage, + destination, + destinationObj + ); + + return [sourceInstance, destinationInstance]; + }, + dispose() { + metadataStorage.dispose(); + mappingStorage.dispose(); + instanceStorage.dispose(); + }, + }; +}; + +function exploreMetadata( + metadataStorage: ClassMetadataStorage, + instanceStorage: ClassInstanceStorage, + ...models: Constructible[] +) { + // Loop through each models passed in + models.forEach((model) => { + // if metadataStorage hasn't had metadata of the model + if (!metadataStorage.has(model)) { + // get the metadata from Reflection then populate metadataStorage and instanceStorage + const metadataList = Reflect.getMetadata('automap:properties', model); + for (const [propertyKey, { typeFn, depth }] of metadataList) { + metadataStorage.addMetadata(model, [propertyKey, typeFn]); + if (depth != null) { + instanceStorage.setDepth(model, propertyKey, depth); + } + } + } + }); +} + +function prePropertiesLoop( + source: Constructible, + metadataStorage: ClassMetadataStorage, + instanceStorage: ClassInstanceStorage, + sourceInstance: unknown, + sourceNestedConstructible: unknown[] +) { + return (mapping: Mapping) => { + // get prototype of the constructor + const sourceProtoConstructor = Object.getPrototypeOf(source.constructor); + // if it has name, then it's not anonymous Function + if (sourceProtoConstructor.name) { + // try to instantiate the proto constructor + const [sourceProtoInstance, sourceProtoNestedConstructible] = instantiate( + instanceStorage, + metadataStorage, + sourceProtoConstructor + ); + // merge the instance of the proto with the sourceInstance + sourceInstance = Object.assign(sourceInstance, sourceProtoInstance); + // update the sourceInstance on the mapping + mapping[MappingClassId.mappings][0] = sourceInstance; + if (sourceProtoNestedConstructible.length) { + // update the nested constructible + sourceNestedConstructible.concat(sourceProtoNestedConstructible); + } + } + }; +} + +function isMultipartSourcePathsInSource(dottedSourcePaths, sourceInstance) { + return !( + dottedSourcePaths.length > 1 && + (!sourceInstance.hasOwnProperty(dottedSourcePaths[0]) || + (sourceInstance[dottedSourcePaths[0]] && + isClass(sourceInstance[dottedSourcePaths[0]]))) + ); } -export default classes; +function isDestinationPathOnSource(sourceProto: unknown) { + return (sourceObj: unknown, sourcePath: string) => { + return !( + !sourceObj.hasOwnProperty(sourcePath) && + !sourceProto.hasOwnProperty(sourcePath) && + !Object.getPrototypeOf(sourceObj).hasOwnProperty(sourcePath) + ); + }; +} diff --git a/packages/classes/src/lib/decorators/automap.decorator.ts b/packages/classes/src/lib/decorators/automap.decorator.ts new file mode 100644 index 000000000..261771710 --- /dev/null +++ b/packages/classes/src/lib/decorators/automap.decorator.ts @@ -0,0 +1,38 @@ +import type { Constructible } from '../types'; + +/** + * AutoMap decorator to decorate fields in classes to store metadata of that field + * + * @param {() => Constructible} [typeFn] - if this is a nested model, it should have a `typeFn` + * @param {number} [depth = 0] - how deep should this model gets instantiated + */ +export const AutoMap = ( + typeFn?: () => Constructible, + depth = 0 +): PropertyDecorator => (target, propertyKey) => { + // If `typeFn` is provided, classes plugin does not have to guess with Reflect and just `defineMetadata` + if (typeFn) { + Reflect.defineMetadata( + 'automap:properties', + [ + ...(Reflect.getMetadata('automap:properties', target.constructor) || + []), + [propertyKey, { typeFn, depth }], + ], + target.constructor + ); + } else { + const meta = Reflect.getMetadata('design:type', target, propertyKey); + if (meta) { + Reflect.defineMetadata( + 'automap:properties', + [ + ...(Reflect.getMetadata('automap:properties', target.constructor) || + []), + [propertyKey, { typeFn: () => meta }], + ], + target.constructor + ); + } + } +}; diff --git a/packages/classes/src/lib/decorators/index.ts b/packages/classes/src/lib/decorators/index.ts new file mode 100644 index 000000000..067927605 --- /dev/null +++ b/packages/classes/src/lib/decorators/index.ts @@ -0,0 +1 @@ +export * from './automap.decorator' diff --git a/packages/classes/src/lib/decorators/specs/automap.decorator.spec.ts b/packages/classes/src/lib/decorators/specs/automap.decorator.spec.ts new file mode 100644 index 000000000..0ab671ea8 --- /dev/null +++ b/packages/classes/src/lib/decorators/specs/automap.decorator.spec.ts @@ -0,0 +1,50 @@ +import 'reflect-metadata'; +import { AutoMap } from '../automap.decorator'; + +describe('AutomapDecorator', () => { + const spiedReflectDefine = jest.spyOn(Reflect, 'defineMetadata'); + const spiedReflectGet = jest.spyOn(Reflect, 'getMetadata'); + + describe('for primitives', () => { + class Bar { + @AutoMap() + bar: string; + } + + it('should call getMetadata', () => { + expect(spiedReflectGet).toHaveBeenCalledWith( + 'design:type', + Bar.prototype, + 'bar' + ); + + spiedReflectGet.mockReset(); + }); + + it('should call defineMetadata', () => { + spiedReflectGet.mockReturnValueOnce(String); + expect(spiedReflectDefine).toHaveBeenCalledWith( + 'automap:properties', + [['bar', { typeFn: expect.any(Function) }]], + Bar + ); + spiedReflectGet.mockReset(); + }); + }); + + describe('for typeFn', () => { + class Foo { + @AutoMap(() => Date) + date: Date; + } + + it('should call defineMetadata', () => { + expect(spiedReflectDefine).toHaveBeenCalledWith( + 'automap:properties', + [['date', { typeFn: expect.any(Function), depth: 0 }]], + Foo + ); + spiedReflectDefine.mockReset(); + }); + }); +}); diff --git a/packages/classes/src/lib/storages/class-instance.storage.ts b/packages/classes/src/lib/storages/class-instance.storage.ts new file mode 100644 index 000000000..9e66b7bb9 --- /dev/null +++ b/packages/classes/src/lib/storages/class-instance.storage.ts @@ -0,0 +1,97 @@ +import type { Constructible } from '../types'; + +/** + * Internal ClassInstanceStorage + * + * This is to store recursive depth and recursive count of circular deps models + * Not test due to private + * + * @private + */ +export class ClassInstanceStorage { + private depthStorage = new WeakMap>(); + private recursiveCountStorage = new WeakMap< + Constructible, + Map + >(); + + getDepthAndCount( + parent: Constructible, + member: string + ): [depth?: number, count?: number] { + return [this.getDepth(parent, member), this.getCount(parent, member)]; + } + + getDepth(parent: Constructible, member: string): number | undefined { + return ClassInstanceStorage.getInternal(this.depthStorage, parent, member); + } + + getCount(parent: Constructible, member: string): number | undefined { + return ClassInstanceStorage.getInternal( + this.recursiveCountStorage, + parent, + member + ); + } + + setDepth(parent: Constructible, member: string, depth: number): void { + ClassInstanceStorage.setInternal(this.depthStorage, parent, member, depth); + } + + setCount(parent: Constructible, member: string, count: number): void { + ClassInstanceStorage.setInternal( + this.recursiveCountStorage, + parent, + member, + count + ); + } + + resetCount(parent: Constructible, member: string): void { + this.setCount(parent, member, 0); + } + + resetAllCount(parent: Constructible): void { + this.recursiveCountStorage.get(parent)?.clear(); + } + + dispose(): void { + this.recursiveCountStorage = new WeakMap< + Constructible, + Map + >(); + this.depthStorage = new WeakMap>(); + } + + private static getInternal( + storage: WeakMap>, + parent: Constructible, + member: string + ): number | undefined { + return storage.get(parent)?.get(member); + } + + private static setInternal( + storage: WeakMap>, + parent: Constructible, + member: string, + value: number + ): void { + if (!storage.has(parent)) { + storage.set(parent, new Map().set(member, value)); + return; + } + + if (!this.hasInternal(storage, parent, member)) { + storage.get(parent)!.set(member, value); + } + } + + private static hasInternal( + storage: WeakMap>, + parent: Constructible, + member: string + ): boolean { + return storage.get(parent)?.has(member) ?? false; + } +} diff --git a/packages/classes/src/lib/storages/class-mapping.storage.ts b/packages/classes/src/lib/storages/class-mapping.storage.ts new file mode 100644 index 000000000..8952faa66 --- /dev/null +++ b/packages/classes/src/lib/storages/class-mapping.storage.ts @@ -0,0 +1,50 @@ +import type { Mapping, MappingStorage } from '@automapper/types'; +import type { Constructible } from '../types'; + +/** + * Internal ClassMappingStorage + * + * This is to store the Mapping when created with Constructibles + * Not test due to private + * + * @private + */ +export class ClassMappingStorage implements MappingStorage { + private storage = new WeakMap< + Constructible, + WeakMap + >(); + + get(source: Constructible, destination: Constructible): Mapping | undefined { + return this.storage.get(source)?.get(destination); + } + + set( + source: Constructible, + destination: Constructible, + mapping: Mapping + ): void { + if (!this.storage.has(source)) { + this.storage.set( + source, + new WeakMap().set(destination, mapping) + ); + return; + } + + if (!this.has(source, destination)) { + this.storage.get(source)!.set(destination, mapping); + } + } + + has(source: Constructible, destination: Constructible): boolean { + return this.storage.get(source)?.has(destination) ?? false; + } + + dispose(): void { + this.storage = new WeakMap< + Constructible, + WeakMap + >(); + } +} diff --git a/packages/classes/src/lib/storages/class-metadata.storage.ts b/packages/classes/src/lib/storages/class-metadata.storage.ts new file mode 100644 index 000000000..7712a9379 --- /dev/null +++ b/packages/classes/src/lib/storages/class-metadata.storage.ts @@ -0,0 +1,73 @@ +import type { Metadata, MetadataStorage } from '@automapper/types'; +import type { Constructible } from '../types'; + +/** + * Internal ClassMetadataStorage + * + * This is to store Metadata of all the models using ReflectMetadata + * Not test due to private + * + * @private + */ +export class ClassMetadataStorage implements MetadataStorage { + private storage = new WeakMap< + Constructible, + Array> + >(); + + getMetadata(model: Constructible): Array> { + const metadataList = this.storage.get(model) ?? []; + let i = metadataList.length; + + // empty metadata + if (!i) { + // try to get the metadata on the prototype of the class + return model.name ? this.getMetadata(Object.getPrototypeOf(model)) : []; + } + + const resultMetadataList: Array> = []; + while (i--) { + const metadata = metadataList[i]; + // skip existing + if (resultMetadataList.some(([metaKey]) => metaKey === metadata[0])) { + continue; + } + resultMetadataList.push(metadataList[i]); + } + + return resultMetadataList; + } + + getMetadataForKey( + model: Constructible, + key: string + ): Metadata | undefined { + return this.getMetadata(model).find(([metaKey]) => metaKey === key); + } + + addMetadata(model: Constructible, metadata: Metadata): void { + // Get metadata on the model + const exists = this.storage.get(model) ?? []; + + // Get metadata on prototype + const protoExists = this.storage.get(Object.getPrototypeOf(model)) ?? []; + + // merged existing metadata + const merged = [...protoExists, ...exists]; + + // if already exists, break + if (merged.some(([existKey]) => existKey === metadata[0])) { + return; + } + + this.storage.set(model, [...merged, metadata]); + } + + has(metaKey: Constructible): boolean { + return this.storage.has(metaKey); + } + + dispose(): void { + this.storage = new WeakMap>>(); + } +} diff --git a/packages/classes/src/lib/storages/index.ts b/packages/classes/src/lib/storages/index.ts new file mode 100644 index 000000000..0f702704d --- /dev/null +++ b/packages/classes/src/lib/storages/index.ts @@ -0,0 +1,3 @@ +export * from './class-metadata.storage'; +export * from './class-mapping.storage'; +export * from './class-instance.storage'; diff --git a/packages/classes/src/lib/types.d.ts b/packages/classes/src/lib/types.d.ts new file mode 100644 index 000000000..55d4d6828 --- /dev/null +++ b/packages/classes/src/lib/types.d.ts @@ -0,0 +1,6 @@ +import { Dictionary, TransformerMetadataFactory } from '@automapper/types'; + +export interface Constructible = unknown> + extends TransformerMetadataFactory { + new (...args: unknown[]): TModel; +} diff --git a/packages/classes/src/lib/utils/index.ts b/packages/classes/src/lib/utils/index.ts new file mode 100644 index 000000000..8d577f8b3 --- /dev/null +++ b/packages/classes/src/lib/utils/index.ts @@ -0,0 +1,4 @@ +export * from './instantiate.util'; +export * from './is-primitive-constructor.util'; +export * from './is-date-constructor.util'; +export * from './is-class.util'; diff --git a/packages/classes/src/lib/utils/instantiate.util.ts b/packages/classes/src/lib/utils/instantiate.util.ts new file mode 100644 index 000000000..f279250ac --- /dev/null +++ b/packages/classes/src/lib/utils/instantiate.util.ts @@ -0,0 +1,133 @@ +import { isDefined, isEmpty } from '@automapper/core'; +import type { Dictionary } from '@automapper/types'; +import type { ClassInstanceStorage, ClassMetadataStorage } from '../storages'; +import type { Constructible } from '../types'; +import { isDateConstructor } from './is-date-constructor.util'; +import { isPrimitiveConstructor } from './is-primitive-constructor.util'; + +/** + * Recursively instantiate a model with its metadata + * + * @param {ClassInstanceStorage} instanceStorage + * @param {ClassMetadataStorage} metadataStorage + * @param {Constructible} model + * @param {TModel} defaultValue + */ +export function instantiate>( + instanceStorage: ClassInstanceStorage, + metadataStorage: ClassMetadataStorage, + model: Constructible, + defaultValue?: TModel +): [TModel, unknown[]?] { + // get the metadata of the model + const metadata = metadataStorage.getMetadata(model); + + // instantiate a model with/without defaultValue + const instance = defaultValue + ? Object.assign(new model(), defaultValue) + : new model(); + + // if metadata is empty, return the instance early + if (isEmpty(metadata) || !metadata) { + return [instance]; + } + + // initialize a nestedConstructible with empty [] + const nestedConstructible = []; + let i = metadata.length; + + // reversed loop + while (i--) { + // destructure + const [key, meta] = metadata[i]; + + // get the value at the current key + const valueAtKey = instance[key]; + + // call the meta fn to get the metaResult of the current key + const metaResult = meta(); + + // if is String, Number, Boolean, assign valueAtKey or undefined + if (isPrimitiveConstructor(metaResult)) { + instance[key] = isDefined(valueAtKey) ? valueAtKey : undefined; + continue; + } + + // if is Date, assign a new Date value + if (isDateConstructor(metaResult)) { + instance[key] = isDefined(valueAtKey) ? new Date(valueAtKey) : new Date(); + continue; + } + + // This current key must be a nested model + // push to nestedConstructible + nestedConstructible.push([key, metaResult as Constructible]); + + // if the value at key is an array + if (Array.isArray(valueAtKey)) { + // loop through each value and recursively call instantiate with each value + instance[key] = valueAtKey.map((val) => { + const [instantiateResult] = instantiate( + instanceStorage, + metadataStorage, + metaResult as Constructible, + val + ); + return instantiateResult; + }); + continue; + } + + // if value is not null/undefined + if (isDefined(valueAtKey)) { + // instantiate with value at key + const [instantiateResult] = instantiate( + instanceStorage, + metadataStorage, + metaResult as Constructible, + valueAtKey + ); + instance[key] = instantiateResult; + continue; + } + + // if value is null/undefined but defaultValue is not + // should assign straightaway + if (isDefined(defaultValue)) { + instance[key] = valueAtKey; + continue; + } + + // get depth and count of the current key on the current model + // Eg: Foo {bar: Bar}, model here is Foo and key is bar + const [depth, count = 0] = instanceStorage.getDepthAndCount(model, key); + + // if no depth, just instantiate with new keyword without recursive + if (depth === 0) { + instance[key] = new (metaResult as Constructible)(); + continue; + } + + // if depth equals count, meaning instantiate has run enough loop. + // reset the count then assign with new keyword + if (depth === count) { + instanceStorage.resetCount(model, key); + instance[key] = new (metaResult as Constructible)(); + continue; + } + + // increment the count and recursively call instantiate + instanceStorage.setCount(model, key, isDefined(count) ? count + 1 : 1); + const [instantiateResult] = instantiate( + instanceStorage, + metadataStorage, + metaResult as Constructible + ); + instance[key] = instantiateResult; + } + + // after all, resetAllCount on the current model + instanceStorage.resetAllCount(model); + // return instance and the nestedConstructible array + return [instance, nestedConstructible]; +} diff --git a/packages/classes/src/lib/utils/is-class.util.ts b/packages/classes/src/lib/utils/is-class.util.ts new file mode 100644 index 000000000..13b651853 --- /dev/null +++ b/packages/classes/src/lib/utils/is-class.util.ts @@ -0,0 +1,16 @@ +/** + * Check for if a passed in `fn` is a Class constructor + * + * @param {Function} fn + */ +export function isClass(fn: unknown): boolean { + const typeOfFn = typeof fn; + const constructorFnString = fn.constructor?.toString(); + return ( + (typeOfFn === 'object' || typeOfFn === 'function') && + fn.constructor && + (/^\s*function/.test(constructorFnString) || + /^\s*class/.test(constructorFnString)) && + constructorFnString.includes(fn.constructor.name) + ); +} diff --git a/packages/classes/src/lib/utils/is-date-constructor.util.ts b/packages/classes/src/lib/utils/is-date-constructor.util.ts new file mode 100644 index 000000000..9d1e56f89 --- /dev/null +++ b/packages/classes/src/lib/utils/is-date-constructor.util.ts @@ -0,0 +1,8 @@ +/** + * Check if value is a Date constructor + * + * @param {Function} value + */ +export function isDateConstructor(value: unknown): boolean { + return value === Date; +} diff --git a/packages/classes/src/lib/utils/is-primitive-constructor.util.ts b/packages/classes/src/lib/utils/is-primitive-constructor.util.ts new file mode 100644 index 000000000..8aa4850d8 --- /dev/null +++ b/packages/classes/src/lib/utils/is-primitive-constructor.util.ts @@ -0,0 +1,8 @@ +/** + * Check if value is a String/Number/Boolean constructor + * + * @param {Function} value + */ +export function isPrimitiveConstructor(value: unknown): boolean { + return value === String || value === Number || value === Boolean; +} diff --git a/packages/core/.eslintrc.json b/packages/core/.eslintrc.json index 54ac39535..717df66a0 100644 --- a/packages/core/.eslintrc.json +++ b/packages/core/.eslintrc.json @@ -1 +1,9 @@ -{ "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } +{ + "extends": "../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "rules": { + "no-prototype-builtins": "off" + } +} diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index 992754225..b0b01fb6a 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -3,7 +3,7 @@ module.exports = { preset: '../../jest.preset.js', globals: { 'ts-jest': { - tsConfig: '/tsconfig.spec.json', + tsconfig: '/tsconfig.spec.json', }, }, testEnvironment: 'node', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b8775802c..774e6b064 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1 +1,4 @@ export * from './lib/create-mapper'; +export * from './lib/utils'; +export * from './lib/member-map-functions'; +export * from './lib/naming-conventions'; diff --git a/packages/core/src/lib/core.spec.ts b/packages/core/src/lib/core.spec.ts deleted file mode 100644 index 290f5f766..000000000 --- a/packages/core/src/lib/core.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { core } from './core'; - -describe('core', () => { - it('should work', () => { - expect(core()).toEqual('core'); - }); -}); diff --git a/packages/core/src/lib/core.ts b/packages/core/src/lib/core.ts deleted file mode 100644 index 54913edb0..000000000 --- a/packages/core/src/lib/core.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function core(): string { - return 'core'; -} diff --git a/packages/core/src/lib/create-mapper.spec.ts b/packages/core/src/lib/create-mapper.spec.ts deleted file mode 100644 index 452fa14ff..000000000 --- a/packages/core/src/lib/create-mapper.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('createMapper', () => { - it('', () => { - expect(true).toBe(true); - }); -}); diff --git a/packages/core/src/lib/create-mapper.ts b/packages/core/src/lib/create-mapper.ts index d9d3c17a5..318988a80 100644 --- a/packages/core/src/lib/create-mapper.ts +++ b/packages/core/src/lib/create-mapper.ts @@ -1 +1,217 @@ -export function createMapper() {} +import type { + CreateMapFluentFunction, + CreateMapOptions, + CreateMapperOptions, + Dictionary, + MapAction, + Mapper, + Mapping, + MappingProfile, + MappingProperty, + MemberMapFunction, + PreConditionFunction, + Selector, + SelectorReturn, +} from '@automapper/types'; +import { + MapFnClassId, + MappingClassId, + MappingPropertiesClassId, + TransformationType, +} from '@automapper/types'; +import { map, mapArray } from './map'; +import { getMemberPath } from './utils'; + +/** + * Method to create a Mapper with a plugin + * + * @param {CreateMapperOptions} createMapperOptions - options for createMapper + */ +export function createMapper({ + name, + pluginInitializer, + namingConventions, + errorHandle: customErrorHandler, +}: CreateMapperOptions): Mapper { + // default errorHandler to console.error + const errorHandler = customErrorHandler || { handle: console.error }; + + // get the plugin + const plugin = pluginInitializer(errorHandler); + + return { + name, + createMap(source, destination, options: CreateMapOptions = {}) { + // if namingConventions isn't passed in for this Mapping pair, use the global ones + if (options && !options.namingConventions) { + options.namingConventions = namingConventions; + } + + // create the initial mapping between source and destination + const mapping = plugin.initializeMapping(source, destination, options); + + // return the FluentFunction for chaining + return createMapFluentFunction(mapping); + }, + getMapping: plugin.getMapping.bind(plugin), + addProfile(profile: MappingProfile) { + profile(this); + return this; + }, + map(sourceObj, destination, source, options?) { + const { preMap } = plugin; + + // run preMap if available + const [sourceInstance] = preMap?.(source, destination, sourceObj) ?? []; + + // get mapping between Source and Destination + const mapping = this.getMapping(source, destination); + + // map + return map( + sourceInstance ?? sourceObj, + mapping, + options, + this, + errorHandler + ); + }, + mapAsync(sourceObj, destination, source, options?) { + return Promise.resolve(this.map(sourceObj, destination, source, options)); + }, + mapArray(sourceArr, destination, source, options) { + return mapArray( + sourceArr, + destination, + source, + options, + this, + errorHandler + ); + }, + mapArrayAsync(sourceArr, destination, source, options) { + return Promise.resolve( + this.mapArray(sourceArr, destination, source, options) + ); + }, + dispose() { + plugin.dispose?.(); + }, + }; +} + +/** + * Method to create FluentFunction for chaining forMember etc... + * + * @param {Mapping} mapping - Mapping object of source <> destination + */ +function createMapFluentFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +>( + mapping: Mapping +): CreateMapFluentFunction { + // initialize fluentFunction + const fluentFunction: CreateMapFluentFunction = { + forMember>( + selector: Selector, + ...functions: [ + ( + | ReturnType> + | ReturnType> + ), + ReturnType>? + ] + ): CreateMapFluentFunction { + return createMapForMember( + mapping, + selector, + functions, + fluentFunction + ); + }, + beforeMap(mapAction: MapAction) { + // assign mapAction to mapping + mapping[MappingClassId.actions][0] = mapAction; + return fluentFunction; + }, + afterMap(mapAction: MapAction) { + // assign mapAction to mapping + mapping[MappingClassId.actions][1] = mapAction; + return fluentFunction; + }, + }; + + return fluentFunction; +} + +/** + * + * @param {Mapping} mapping - Mapping between source <> destination + * @param {Selector} selector - the member selector on `forMember(selector)` + * @param preCond + * @param mapMemberFn + * @param fluentFunction + */ +function createMapForMember< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TMemberType = SelectorReturn +>( + mapping: Mapping, + selector: Selector, + [preCond, mapMemberFn]: [ + preCond: + | ReturnType> + | ReturnType>, + mapMemberFn?: ReturnType> + ], + fluentFunction: CreateMapFluentFunction +): CreateMapFluentFunction { + // get the memberPath from the selector + // eg: `s => s.foo.bar` returns `foo.bar` + const memberPath = getMemberPath(selector); + + // reassign mapMemberFn and preCond + if (mapMemberFn == null) { + mapMemberFn = preCond as ReturnType; + preCond = undefined; + } + + // initialize sourcePath + let sourcePath = ''; + + // if the transformation is MapFrom or MapWith, we have information on the source value selector + if ( + mapMemberFn[MapFnClassId.type] === TransformationType.MapFrom || + mapMemberFn[MapFnClassId.type] === TransformationType.MapWith + ) { + sourcePath = getMemberPath(mapMemberFn[MapFnClassId.misc]); + } + + // initialize paths tuple + const paths: [member: string, source?: string] = !sourcePath + ? [memberPath] + : [memberPath, sourcePath]; + + // initialize MappingProperty + const mappingProperty: MappingProperty = [ + paths, + [mapMemberFn, preCond as ReturnType], + ]; + + // check existProp on mapping + const existProp = mapping[MappingClassId.properties].find( + ([propName]) => propName === memberPath + ); + + // if exists, overrides + if (existProp != null) { + existProp[MappingPropertiesClassId.property] = mappingProperty; + return fluentFunction; + } + + // push MappingProperty to mapping + mapping[MappingClassId.properties].push([memberPath, mappingProperty]); + return fluentFunction; +} diff --git a/packages/core/src/lib/map.ts b/packages/core/src/lib/map.ts new file mode 100644 index 000000000..73a0da068 --- /dev/null +++ b/packages/core/src/lib/map.ts @@ -0,0 +1,326 @@ +/* eslint-disable prefer-const */ +import type { + ConditionFunction, + ConvertUsingFunction, + Dictionary, + ErrorHandler, + FromValueFunction, + MapDeferFunction, + MapFromFunction, + MapInitializeFunction, + MapOptions, + Mapper, + Mapping, + MapWithFunction, + MemberMapFunction, + NullSubstitutionFunction, +} from '@automapper/types'; +import { MapFnClassId, TransformationType } from '@automapper/types'; +import { isEmpty, set } from './utils'; + +/** + * Instruction on how to map a particular member on the destination + * + * @param {ReturnType} transformationMapFn - Transformation information of the property + * @param {TSource} sourceObj - The sourceObject being used to map to destination + * @param destination - destination meta key + * @param {string} destinationMemberPath - the property path on the destination + * @param {Mapper} mapper - the mapper instance + */ +function mapMember = unknown>( + transformationMapFn: ReturnType, + sourceObj: TSource, + destination: unknown, + destinationMemberPath: string, + mapper: Mapper +) { + let value: unknown; + const transformationType: TransformationType = + transformationMapFn[MapFnClassId.type]; + const mapFn = transformationMapFn[MapFnClassId.fn]; + + switch (transformationType) { + case TransformationType.MapFrom: + value = (mapFn as ReturnType[MapFnClassId.fn])( + sourceObj, + destination + ); + break; + case TransformationType.FromValue: + value = (mapFn as ReturnType[MapFnClassId.fn])(); + break; + case TransformationType.MapWith: + value = (mapFn as ReturnType[MapFnClassId.fn])( + sourceObj, + mapper + ); + break; + case TransformationType.ConvertUsing: + value = (mapFn as ReturnType[MapFnClassId.fn])( + sourceObj + ); + break; + case TransformationType.Condition: + case TransformationType.NullSubstitution: + value = (mapFn as ReturnType< + NullSubstitutionFunction | ConditionFunction + >[MapFnClassId.fn])(sourceObj, destinationMemberPath); + break; + case TransformationType.MapDefer: + value = mapMember( + (mapFn as ReturnType[MapFnClassId.fn])( + sourceObj + ) as ReturnType, + sourceObj, + destination, + destinationMemberPath, + mapper + ); + break; + } + return value; +} + +/** + * Depends on implementation of plugin.initializeMapping + */ +function assertUnmappedProperties< + TDestination extends Dictionary = unknown +>( + destination: TDestination, + configuredKeys: string[], + errorHandler: ErrorHandler +) { + const unmappedKeys = Object.keys(destination).filter( + (k) => !configuredKeys.includes(k) + ); + if (unmappedKeys.length) { + errorHandler.handle(` +Unmapped properties: +------------------- +${unmappedKeys.join(',\n')} +`); + } +} + +/** + * + * @param {TSource} sourceObj - the source object + * @param {Mapping} mapping - the Mapping object of source <> destination + * @param {MapOptions} options - options used for this particular map operation + * @param {Mapper} mapper - the mapper instance + * @param {ErrorHandler} errorHandler - the error handler + * @param {boolean} [isMapArray = false] - whether the map operation is in Array mode + */ +export function map< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +>( + sourceObj: TSource, + mapping: Mapping, + options: MapOptions, + mapper: Mapper, + errorHandler: ErrorHandler, + isMapArray = false +) { + // destructure the mapping + let [ + [, destination], + propsToMap, + [mappingBeforeAction, mappingAfterAction], + ] = mapping; + + // initialize an array of keys that have already been configured + const configuredKeys: string[] = []; + + // deconstruct MapOptions + const { beforeMap: mapBeforeAction, afterMap: mapAfterAction } = + options ?? {}; + + // Before Map + // Do not run before map when in Map Array mode + if (!isMapArray) { + const beforeMap = mapBeforeAction ?? mappingBeforeAction; + beforeMap?.(sourceObj, destination); + } + + // map + let i = propsToMap.length; + while (i--) { + // Destructure a props on Mapping which is [propertyKey, MappingProperty, nested?] + const [ + destinationMemberPath, + [ + , + [ + transformationMapFn, + [ + transformationPreCondPredicate, + preCondDefaultValue = undefined, + ] = [], + ], + ], + [nestedDestinationMemberKey, nestedSourceMemberKey] = [], + ] = propsToMap[i]; + + // Setup a shortcut function to set destinationMemberPath on destination with value as argument + const setMember = (value: unknown) => + set(destination, destinationMemberPath, value); + + // This destination key is being configured. Push to configuredKeys array + configuredKeys.push(destinationMemberPath); + + // Pre Condition check + if ( + transformationPreCondPredicate && + !transformationPreCondPredicate(sourceObj) + ) { + destination = setMember(preCondDefaultValue); + continue; + } + + // Start with all the mapInitialize + if ( + transformationMapFn[MapFnClassId.type] === + TransformationType.MapInitialize + ) { + const mapInitializedValue = (transformationMapFn[ + MapFnClassId.fn + ] as ReturnType[MapFnClassId.fn])(sourceObj); + + // if null/undefined + if (mapInitializedValue == null) { + destination = setMember(mapInitializedValue); + continue; + } + + // if isDate + if (mapInitializedValue instanceof Date) { + destination = setMember(new Date(mapInitializedValue)); + continue; + } + + // if isArray + if (Array.isArray(mapInitializedValue)) { + const [first] = mapInitializedValue; + // if first item is a primitive + if (typeof first !== 'object') { + destination = setMember(mapInitializedValue.slice()); + continue; + } + + // if first is empty + if (isEmpty(first)) { + destination = setMember([]); + continue; + } + + destination = setMember( + mapArray( + mapInitializedValue, + nestedDestinationMemberKey, + nestedSourceMemberKey, + undefined, + mapper, + errorHandler + ) + ); + continue; + } + + // if is object + if (typeof mapInitializedValue === 'object') { + const nestedMapping = mapper.getMapping( + nestedSourceMemberKey, + nestedDestinationMemberKey + ); + destination = setMember( + map( + mapInitializedValue, + nestedMapping, + undefined, + mapper, + errorHandler + ) + ); + continue; + } + + // if is primitive + destination = setMember(mapInitializedValue); + continue; + } + + destination = setMember( + mapMember( + transformationMapFn, + sourceObj, + destination, + destinationMemberPath, + mapper + ) + ); + } + + // After map + // Do not map for when in Map Array mode + if (!isMapArray) { + const afterMap = mapAfterAction ?? mappingAfterAction; + afterMap?.(sourceObj, destination); + } + + // Check unmapped properties + assertUnmappedProperties(destination, configuredKeys, errorHandler); + + return destination; +} + +/** + * + * @param {TSource[]} sourceArray - the array of source items to map + * @param destination - destination meta key + * @param source - source meta key + * @param {MapOptions} options - the map options for this particular map operation + * @param {Mapper} mapper - the mapper instance + * @param {ErrorHandler} errorHandler - error handler + */ +export function mapArray< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +>( + sourceArray: TSource[], + destination: unknown, + source: unknown, + options: MapOptions, + mapper: Mapper, + errorHandler: ErrorHandler +) { + // intialize an empty array + let destinationArray: TDestination[] = []; + + // destructure mapOptions + const { beforeMap, afterMap } = options ?? {}; + + // run beforeMap for the whole map operation + beforeMap?.(sourceArray, []); + + // loop through each item and run map() for each + for (let i = 0, len = sourceArray.length; i < len; i++) { + const mapping = mapper.getMapping(source, destination); + destinationArray.push( + map( + sourceArray[i], + mapping as Mapping, + undefined, + mapper, + errorHandler, + true + ) + ); + } + + // run afterMap for the whole map operation + afterMap?.(sourceArray, destinationArray); + + return destinationArray; +} diff --git a/packages/core/src/lib/member-map-functions/condition.ts b/packages/core/src/lib/member-map-functions/condition.ts new file mode 100644 index 000000000..5bad5ea1b --- /dev/null +++ b/packages/core/src/lib/member-map-functions/condition.ts @@ -0,0 +1,29 @@ +import type { + ConditionFunction, + ConditionPredicate, + Dictionary, + SelectorReturn, +} from '@automapper/types'; +import { TransformationType } from '@automapper/types'; +import { get } from '../utils'; + +export function condition< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + predicate: ConditionPredicate, + defaultValue?: TSelectorReturn +): ReturnType> { + return [ + TransformationType.Condition, + null, + (source, ...sourceMemberPaths) => { + if (predicate(source)) { + return get(source, ...sourceMemberPaths) as TSelectorReturn; + } + + return defaultValue; + }, + ]; +} diff --git a/packages/core/src/lib/member-map-functions/convert-using.ts b/packages/core/src/lib/member-map-functions/convert-using.ts new file mode 100644 index 000000000..32df71d7a --- /dev/null +++ b/packages/core/src/lib/member-map-functions/convert-using.ts @@ -0,0 +1,29 @@ +import type { + Converter, + ConvertUsingFunction, + Dictionary, + Selector, + SelectorReturn, +} from '@automapper/types'; +import { TransformationType } from '@automapper/types'; + +export function convertUsing< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn, + TConvertSource = TSource +>( + converter: Converter, + value?: Selector +): ReturnType> { + return [ + TransformationType.ConvertUsing, + null, + (source) => { + const valueToConvert: TConvertSource = value + ? value(source) + : ((source as unknown) as TConvertSource); + return converter.convert(valueToConvert); + }, + ]; +} diff --git a/packages/core/src/lib/member-map-functions/from-value.ts b/packages/core/src/lib/member-map-functions/from-value.ts new file mode 100644 index 000000000..9ad0fd612 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/from-value.ts @@ -0,0 +1,16 @@ +import type { + Dictionary, + FromValueFunction, + SelectorReturn, +} from '@automapper/types'; +import { TransformationType } from '@automapper/types'; + +export function fromValue< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + rawValue: TSelectorReturn +): ReturnType> { + return [TransformationType.FromValue, null, () => rawValue]; +} diff --git a/packages/core/src/lib/member-map-functions/ignore.ts b/packages/core/src/lib/member-map-functions/ignore.ts new file mode 100644 index 000000000..6bf4a8142 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/ignore.ts @@ -0,0 +1,6 @@ +import type { IgnoreFunction } from '@automapper/types'; +import { TransformationType } from '@automapper/types'; + +export function ignore(): ReturnType { + return [TransformationType.Ignore, null, null]; +} diff --git a/packages/core/src/lib/member-map-functions/index.ts b/packages/core/src/lib/member-map-functions/index.ts new file mode 100644 index 000000000..808ecb8db --- /dev/null +++ b/packages/core/src/lib/member-map-functions/index.ts @@ -0,0 +1,10 @@ +export * from './map-initialize'; +export * from './map-from'; +export * from './map-with'; +export * from './condition'; +export * from './convert-using'; +export * from './from-value'; +export * from './ignore'; +export * from './null-substitution'; +export * from './pre-condition'; +export * from './map-defer'; diff --git a/packages/core/src/lib/member-map-functions/map-defer.ts b/packages/core/src/lib/member-map-functions/map-defer.ts new file mode 100644 index 000000000..a8c06f308 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/map-defer.ts @@ -0,0 +1,16 @@ +import type { + DeferFunction, + Dictionary, + MapDeferFunction, +} from '@automapper/types'; +import { SelectorReturn, TransformationType } from '@automapper/types'; + +export function mapDefer< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + defer: DeferFunction +): ReturnType> { + return [TransformationType.MapDefer, null, defer]; +} diff --git a/packages/core/src/lib/member-map-functions/map-from.ts b/packages/core/src/lib/member-map-functions/map-from.ts new file mode 100644 index 000000000..e00a3bd6b --- /dev/null +++ b/packages/core/src/lib/member-map-functions/map-from.ts @@ -0,0 +1,28 @@ +import type { + Dictionary, + MapFromFunction, + Resolver, + SelectorReturn, + ValueSelector, +} from '@automapper/types'; +import { TransformationType } from '@automapper/types'; + +export function mapFrom< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + from: + | ValueSelector + | Resolver +): ReturnType> { + if (isResolver(from)) { + return [TransformationType.MapFrom, null, from.resolve.bind(from)]; + } + + return [TransformationType.MapFrom, from, from]; +} + +function isResolver(fn: ValueSelector | Resolver): fn is Resolver { + return 'resolve' in fn; +} diff --git a/packages/core/src/lib/member-map-functions/map-initialize.ts b/packages/core/src/lib/member-map-functions/map-initialize.ts new file mode 100644 index 000000000..9469ca10f --- /dev/null +++ b/packages/core/src/lib/member-map-functions/map-initialize.ts @@ -0,0 +1,21 @@ +import type { + Dictionary, + MapInitializeFunction, + SelectorReturn, +} from '@automapper/types'; +import { TransformationType } from '@automapper/types'; +import { get } from '../utils'; + +export function mapInitialize< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + ...sourcePaths: string[] +): ReturnType> { + return [ + TransformationType.MapInitialize, + null, + (source) => get(source, ...sourcePaths) as TSelectorReturn, + ]; +} diff --git a/packages/core/src/lib/member-map-functions/map-with.ts b/packages/core/src/lib/member-map-functions/map-with.ts new file mode 100644 index 000000000..013ae15b4 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/map-with.ts @@ -0,0 +1,34 @@ +import type { + Dictionary, + MapWithFunction, + SelectorReturn, +} from '@automapper/types'; +import { + Fn, + TransformationType, + Unpacked, + ValueSelector, +} from '@automapper/types'; + +export function mapWith< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + withDestination: Fn>, + withSourceValue: ValueSelector, + withSource: Fn +): ReturnType> { + return [ + TransformationType.MapWith, + withSourceValue, + (source, mapper) => { + const sourceValue = withSourceValue(source); + return mapper.map( + sourceValue, + withDestination() as string, + withSource() as string + ); + }, + ]; +} diff --git a/packages/core/src/lib/member-map-functions/null-substitution.ts b/packages/core/src/lib/member-map-functions/null-substitution.ts new file mode 100644 index 000000000..63fa1adc4 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/null-substitution.ts @@ -0,0 +1,24 @@ +import type { + Dictionary, + NullSubstitutionFunction, + SelectorReturn, +} from '@automapper/types'; +import { TransformationType } from '@automapper/types'; +import { get } from '../utils'; + +export function nullSubstitution< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + substitution: TSelectorReturn +): ReturnType< + NullSubstitutionFunction +> { + return [ + TransformationType.NullSubstitution, + null, + (source, ...sourceMemberPaths) => + (get(source, ...sourceMemberPaths) as TSelectorReturn) ?? substitution, + ]; +} diff --git a/packages/core/src/lib/member-map-functions/pre-condition.ts b/packages/core/src/lib/member-map-functions/pre-condition.ts new file mode 100644 index 000000000..616e9b998 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/pre-condition.ts @@ -0,0 +1,17 @@ +import type { + ConditionPredicate, + Dictionary, + PreConditionFunction, + SelectorReturn, +} from '@automapper/types'; + +export function preCondition< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +>( + predicate: ConditionPredicate, + defaultValue?: TSelectorReturn +): ReturnType> { + return [predicate, defaultValue]; +} diff --git a/packages/core/src/lib/member-map-functions/specs/condition.spec.ts b/packages/core/src/lib/member-map-functions/specs/condition.spec.ts new file mode 100644 index 000000000..9ab8d9656 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/condition.spec.ts @@ -0,0 +1,42 @@ +import { condition } from '@automapper/core'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('ConditionFunction', () => { + const source = { + toMap: 'truthy', + }; + + it('should return correctly', () => { + const conditionFn = condition(() => true); + expect(conditionFn).toBeTruthy(); + expect(conditionFn[MapFnClassId.type]).toEqual( + TransformationType.Condition + ); + expect(conditionFn[MapFnClassId.misc]).toEqual(null); + expect(conditionFn[MapFnClassId.fn]).toBeInstanceOf(Function); + }); + + it('should map to source.truthy when evaluated to true', () => { + const conditionFn = condition(() => true); + const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + expect(result).toEqual(source.toMap); + }); + + it('should map to source.truthy when evaluated to true regardless of defaultValue', () => { + const conditionFn = condition(() => true, 'defaultValue'); + const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + expect(result).toEqual(source.toMap); + }); + + it('should map to undefined when evaluated to false', () => { + const conditionFn = condition(() => false); + const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + expect(result).toEqual(undefined); + }); + + it('should map to defaultValue when evaluated to false and defaultValue is provided', () => { + const conditionFn = condition(() => false, 'defaultValue'); + const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + expect(result).toEqual('defaultValue'); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/convert-using.spec.ts b/packages/core/src/lib/member-map-functions/specs/convert-using.spec.ts new file mode 100644 index 000000000..c39dfb8ad --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/convert-using.spec.ts @@ -0,0 +1,49 @@ +import { convertUsing } from '@automapper/core'; +import type { Converter } from '@automapper/types'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('ConvertUsingFunction', () => { + const dateToStringConverter: Converter = { + convert(source: Date): string { + return source.toDateString(); + }, + }; + + const birthdayToStringConverter: Converter<{ birthday: Date }, string> = { + convert(source: { birthday: Date }): string { + return source.birthday.toDateString(); + }, + }; + + const source: { birthday: Date; birth?: Date } = { + birthday: new Date('10/14/1991'), + }; + + it('should return correctly', () => { + const convertUsingFn = convertUsing( + dateToStringConverter, + () => new Date() + ); + expect(convertUsingFn).toBeTruthy(); + expect(convertUsingFn[MapFnClassId.type]).toEqual( + TransformationType.ConvertUsing + ); + expect(convertUsingFn[MapFnClassId.misc]).toEqual(null); + expect(convertUsingFn[MapFnClassId.fn]).toBeInstanceOf(Function); + }); + + it('should map correctly', () => { + const convertUsingFn = convertUsing( + dateToStringConverter, + (s: typeof source) => s.birthday + ); + const result = convertUsingFn[MapFnClassId.fn](source); + expect(result).toEqual(source.birthday.toDateString()); + }); + + it('should map correctly with source', () => { + const convertUsingFn = convertUsing(birthdayToStringConverter); + const result = convertUsingFn[MapFnClassId.fn](source); + expect(result).toEqual(source.birthday.toDateString()); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/from-value.spec.ts b/packages/core/src/lib/member-map-functions/specs/from-value.spec.ts new file mode 100644 index 000000000..976816fcb --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/from-value.spec.ts @@ -0,0 +1,20 @@ +import { fromValue } from '@automapper/core'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('FromValueFunction', () => { + it('should return correctly', () => { + const fromValueFunction = fromValue(10); + expect(fromValueFunction).toBeTruthy(); + expect(fromValueFunction[MapFnClassId.type]).toEqual( + TransformationType.FromValue + ); + expect(fromValueFunction[MapFnClassId.misc]).toEqual(null); + expect(fromValueFunction[MapFnClassId.fn]).toBeInstanceOf(Function); + }); + + it('should map correctly', () => { + const fromValueFunction = fromValue(10); + const result = fromValueFunction[MapFnClassId.fn](); + expect(result).toEqual(10); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/ignore.spec.ts b/packages/core/src/lib/member-map-functions/specs/ignore.spec.ts new file mode 100644 index 000000000..8e47d5cbb --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/ignore.spec.ts @@ -0,0 +1,12 @@ +import { ignore } from '@automapper/core'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('IgnoreFunction', () => { + it('should return correctly', () => { + const ignoreFn = ignore(); + expect(ignoreFn).toBeTruthy(); + expect(ignoreFn[MapFnClassId.type]).toEqual(TransformationType.Ignore); + expect(ignoreFn[MapFnClassId.misc]).toEqual(null); + expect(ignoreFn[MapFnClassId.fn]).toEqual(null); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/map-defer.spec.ts b/packages/core/src/lib/member-map-functions/specs/map-defer.spec.ts new file mode 100644 index 000000000..2d8021e25 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/map-defer.spec.ts @@ -0,0 +1,15 @@ +import { ignore, mapDefer } from '@automapper/core'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('MapDeferFunction', () => { + it('should return correctly', () => { + const defer = () => ignore(); + + const mapDeferFn = mapDefer(defer); + + expect(mapDeferFn).toBeTruthy(); + expect(mapDeferFn[MapFnClassId.type]).toEqual(TransformationType.MapDefer); + expect(mapDeferFn[MapFnClassId.misc]).toEqual(null); + expect(mapDeferFn[MapFnClassId.fn]).toBe(defer); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/map-from.spec.ts b/packages/core/src/lib/member-map-functions/specs/map-from.spec.ts new file mode 100644 index 000000000..19038f34a --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/map-from.spec.ts @@ -0,0 +1,37 @@ +import { mapFrom } from '@automapper/core'; +import type { Resolver } from '@automapper/types'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('MapFromFunction', () => { + const source = { + foo: 'bar', + }; + + const sourceSelector = (s: typeof source) => s.foo; + + it('should return correctly', () => { + const mapFromFn = mapFrom(sourceSelector); + expect(mapFromFn).toBeTruthy(); + expect(mapFromFn[MapFnClassId.type]).toEqual(TransformationType.MapFrom); + expect(mapFromFn[MapFnClassId.misc]).toEqual(sourceSelector); + expect(mapFromFn[MapFnClassId.fn]).toBeInstanceOf(Function); + }); + + it('should map to foo correctly', () => { + const mapFromFn = mapFrom(sourceSelector); + const result = mapFromFn[MapFnClassId.fn](source); + expect(result).toEqual(source.foo); + }); + + const resolver: Resolver = { + resolve(source: { foo: string }) { + return source.foo; + }, + }; + + it('should use resolver correctly', () => { + const mapFromFn = mapFrom(resolver); + const result = mapFromFn[MapFnClassId.fn](source, null); + expect(result).toEqual(source.foo); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/map-initialize.spec.ts b/packages/core/src/lib/member-map-functions/specs/map-initialize.spec.ts new file mode 100644 index 000000000..354d34b85 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/map-initialize.spec.ts @@ -0,0 +1,32 @@ +import { mapInitialize } from '@automapper/core'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('MapInitializeFunction', () => { + const source = { + foo: { + bar: 'foo', + }, + }; + + it('should return correctly', () => { + const mapInitFn = mapInitialize(null); + expect(mapInitFn).toBeTruthy(); + expect(mapInitFn[MapFnClassId.type]).toEqual( + TransformationType.MapInitialize + ); + expect(mapInitFn[MapFnClassId.misc]).toEqual(null); + expect(mapInitFn[MapFnClassId.fn]).toBeInstanceOf(Function); + }); + + it('should map correctly', () => { + const mapInitFn = mapInitialize('foo.bar'); + const result = mapInitFn[MapFnClassId.fn](source); + expect(result).toEqual(source.foo.bar); + }); + + it('should map to undefined for default val', () => { + const mapInitFn = mapInitialize('foo.baz'); + const result = mapInitFn[MapFnClassId.fn](source); + expect(result).toEqual(undefined); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/map-with.spec.ts b/packages/core/src/lib/member-map-functions/specs/map-with.spec.ts new file mode 100644 index 000000000..bdcfd9adf --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/map-with.spec.ts @@ -0,0 +1,29 @@ +import { mapWith } from '@automapper/core'; +import type { Mapper } from '@automapper/types'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('MapWithFunction', () => { + const selector = (s) => s; + const withDestination = () => ''; + const withSource = () => ''; + + const mapper = { map: jest.fn() }; + + it('should return correctly', () => { + const mapWithFn = mapWith(withDestination, selector, withSource); + expect(mapWithFn).toBeTruthy(); + expect(mapWithFn[MapFnClassId.type]).toEqual(TransformationType.MapWith); + expect(mapWithFn[MapFnClassId.misc]).toBe(selector); + expect(mapWithFn[MapFnClassId.fn]).toBeInstanceOf(Function); + }); + + it('should call mapper.map', () => { + const mapWithFn = mapWith(withDestination, selector, withSource); + mapWithFn[MapFnClassId.fn]({}, (mapper as unknown) as Mapper); + expect(mapper.map).toHaveBeenCalledWith( + {}, + withDestination(), + withSource() + ); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/null-substitution.spec.ts b/packages/core/src/lib/member-map-functions/specs/null-substitution.spec.ts new file mode 100644 index 000000000..4eaa75271 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/null-substitution.spec.ts @@ -0,0 +1,28 @@ +import { nullSubstitution } from '@automapper/core'; +import { MapFnClassId, TransformationType } from '@automapper/types'; + +describe('NullSubstitutionFunction', () => { + it('should return correctly', () => { + const nullSubFn = nullSubstitution(''); + expect(nullSubFn).toBeTruthy(); + expect(nullSubFn[MapFnClassId.type]).toEqual( + TransformationType.NullSubstitution + ); + expect(nullSubFn[MapFnClassId.misc]).toEqual(null); + expect(nullSubFn[MapFnClassId.fn]).toBeInstanceOf(Function); + }); + + it('should return source if source is not null', () => { + const nullSubFn = nullSubstitution('subbed'); + const result = nullSubFn[MapFnClassId.fn]({ foo: 'bar' }, 'foo'); + expect(result).toEqual('bar'); + expect(result).not.toEqual('subbed'); + }); + + it('should return subbed if source is null', () => { + const nullSubFn = nullSubstitution('subbed'); + const result = nullSubFn[MapFnClassId.fn]({ foo: null }, 'foo'); + expect(result).toEqual('subbed'); + expect(result).not.toEqual(null); + }); +}); diff --git a/packages/core/src/lib/member-map-functions/specs/pre-condition.spec.ts b/packages/core/src/lib/member-map-functions/specs/pre-condition.spec.ts new file mode 100644 index 000000000..c5d7792d1 --- /dev/null +++ b/packages/core/src/lib/member-map-functions/specs/pre-condition.spec.ts @@ -0,0 +1,25 @@ +import { preCondition } from '@automapper/core'; + +describe('PreConditionFunction', () => { + it('should return correctly', () => { + let preCondFn = preCondition(() => true); + expect(preCondFn).toBeTruthy(); + expect(preCondFn?.[0]).toBeInstanceOf(Function); + expect(preCondFn?.[1]).toEqual(undefined); + + preCondFn = preCondition(() => true, 'default'); + expect(preCondFn?.[1]).toEqual('default'); + }); + + it('should return truthy when true', () => { + const preCond = preCondition(() => true); + const result = preCond?.[0]({}); + expect(result).toEqual(true); + }); + + it('should return falsy when false', () => { + const preCond = preCondition(() => false); + const result = preCond?.[0]({}); + expect(result).toEqual(false); + }); +}); diff --git a/packages/core/src/lib/naming-conventions/camel-case-naming-convention.ts b/packages/core/src/lib/naming-conventions/camel-case-naming-convention.ts index 46312ea52..2f260a370 100644 --- a/packages/core/src/lib/naming-conventions/camel-case-naming-convention.ts +++ b/packages/core/src/lib/naming-conventions/camel-case-naming-convention.ts @@ -1,4 +1,4 @@ -import { NamingConvention } from '@automapper/models'; +import { NamingConvention } from '@automapper/types'; /** * CamelCaseNamingConvention diff --git a/packages/core/src/lib/naming-conventions/pascal-case-naming-convention.ts b/packages/core/src/lib/naming-conventions/pascal-case-naming-convention.ts index ccd9b3053..0108377fb 100644 --- a/packages/core/src/lib/naming-conventions/pascal-case-naming-convention.ts +++ b/packages/core/src/lib/naming-conventions/pascal-case-naming-convention.ts @@ -1,4 +1,4 @@ -import { NamingConvention } from '@automapper/models'; +import { NamingConvention } from '@automapper/types'; /** * PascalCaseNamingConvention diff --git a/packages/core/src/lib/naming-conventions/snake-case-naming-convention.ts b/packages/core/src/lib/naming-conventions/snake-case-naming-convention.ts index e02b23321..473940bea 100644 --- a/packages/core/src/lib/naming-conventions/snake-case-naming-convention.ts +++ b/packages/core/src/lib/naming-conventions/snake-case-naming-convention.ts @@ -1,4 +1,4 @@ -import { NamingConvention } from '@automapper/models'; +import { NamingConvention } from '@automapper/types'; /** * SnakeCaseNamingConvention diff --git a/packages/core/src/lib/naming-conventions/specs/camel-case-naming-convention.spec.ts b/packages/core/src/lib/naming-conventions/specs/camel-case-naming-convention.spec.ts index a00b8a57c..c823f436c 100644 --- a/packages/core/src/lib/naming-conventions/specs/camel-case-naming-convention.spec.ts +++ b/packages/core/src/lib/naming-conventions/specs/camel-case-naming-convention.spec.ts @@ -1,4 +1,4 @@ -import { CamelCaseNamingConvention } from '../camel-case-naming-convention'; +import { CamelCaseNamingConvention } from '@automapper/core'; describe('CamelCaseNamingConvention', () => { const one = ['Address']; diff --git a/packages/core/src/lib/naming-conventions/specs/pascal-case-naming-convention.spec.ts b/packages/core/src/lib/naming-conventions/specs/pascal-case-naming-convention.spec.ts index 39e554ed3..d2293b6b7 100644 --- a/packages/core/src/lib/naming-conventions/specs/pascal-case-naming-convention.spec.ts +++ b/packages/core/src/lib/naming-conventions/specs/pascal-case-naming-convention.spec.ts @@ -1,4 +1,4 @@ -import { PascalCaseNamingConvention } from '../pascal-case-naming-convention'; +import { PascalCaseNamingConvention } from '@automapper/core'; describe('PascalCaseNamingConvention', () => { const one = ['address']; diff --git a/packages/core/src/lib/naming-conventions/specs/snake-case-naming-convention.spec.ts b/packages/core/src/lib/naming-conventions/specs/snake-case-naming-convention.spec.ts index 7648cf65b..831b34764 100644 --- a/packages/core/src/lib/naming-conventions/specs/snake-case-naming-convention.spec.ts +++ b/packages/core/src/lib/naming-conventions/specs/snake-case-naming-convention.spec.ts @@ -1,4 +1,4 @@ -import { SnakeCaseNamingConvention } from '../snake-case-naming-convention'; +import { SnakeCaseNamingConvention } from '@automapper/core'; describe('SnakeCaseNamingConvention', () => { const one = ['address']; diff --git a/packages/core/src/lib/storages/index.ts b/packages/core/src/lib/storages/index.ts deleted file mode 100644 index 29a1c7d4b..000000000 --- a/packages/core/src/lib/storages/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './mapping.storage'; -export * from './profile.storage'; -export * from './metadata.storage'; diff --git a/packages/core/src/lib/storages/mapping.storage.ts b/packages/core/src/lib/storages/mapping.storage.ts deleted file mode 100644 index 93a1bb3c3..000000000 --- a/packages/core/src/lib/storages/mapping.storage.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AutoMapperStorage } from '@automapper/models'; - -/** - * Internal MappingStorage - * - * @private - */ -export class MappingStorage extends AutoMapperStorage<'mapping'> { - protected storage: WeakMap>; - - protected getInternal( - storage: WeakMap>, - entryKey: any, - nestedEntryKey: any - ): WeakMap { - return storage.get(entryKey)?.get(nestedEntryKey); - } - - protected hasInternal( - storage: WeakMap>, - entryKey: any, - nestedEntryKey: any - ): boolean { - return storage.get(entryKey)?.has(nestedEntryKey) ?? false; - } - - protected setInternal( - storage: WeakMap>, - entryKey: any, - [nestedEntryKey, nestedEntryValue]: [any, WeakMap] - ): void { - if (!storage.has(entryKey)) { - storage.set( - entryKey, - new WeakMap().set(nestedEntryKey, nestedEntryValue) - ); - return; - } - - if (!this.hasInternal(storage, entryKey, nestedEntryKey)) { - storage.get(entryKey).set(nestedEntryKey, nestedEntryValue); - } - } -} diff --git a/packages/core/src/lib/storages/metadata.storage.ts b/packages/core/src/lib/storages/metadata.storage.ts deleted file mode 100644 index 7de762517..000000000 --- a/packages/core/src/lib/storages/metadata.storage.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Internal MetadataStorage - * - * One and only one instance of MetadataStorage at any given time - * A MetadataStorage will store all information about metadata - * of all models inside of an application. - * - * @private - */ -class MetadataStorage { - private storage = new WeakMap(); -} - -export const metadataStorage = new MetadataStorage(); diff --git a/packages/core/src/lib/storages/profile.storage.ts b/packages/core/src/lib/storages/profile.storage.ts deleted file mode 100644 index 43527438a..000000000 --- a/packages/core/src/lib/storages/profile.storage.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Internal ProfileStorage - * - * @private - */ -export class ProfileStorage {} diff --git a/packages/core/src/lib/utils/create-initial-mapping.util.ts b/packages/core/src/lib/utils/create-initial-mapping.util.ts new file mode 100644 index 000000000..e8e8eb8f6 --- /dev/null +++ b/packages/core/src/lib/utils/create-initial-mapping.util.ts @@ -0,0 +1,143 @@ +import type { + CreateMapOptions, + Mapping, + MappingProperty, +} from '@automapper/types'; +import { MappingClassId } from '@automapper/types'; +import { mapInitialize } from '../member-map-functions'; +import { extendMappings } from './extend-mappings.util'; +import { getFlatteningSourcePaths } from './get-flattening-source-paths.util'; +import { getNamingConventionsFromOptions } from './get-naming-conventions-from-options.util'; +import { getNestedMetaKeyAtDestinationPath } from './get-nested-metakey-at-destination-path.util'; +import { getPathRecursive } from './get-path-recursive.util'; +import { getSourcePropertyPath } from './get-source-property-path.util'; +import { isDefined } from './is-defined.util'; + +function defaultIsMultipartSourcePathsInSource( + multipartSourcePaths: string[], + sourceObj: unknown +): boolean { + return !( + multipartSourcePaths.length > 1 && + (!sourceObj.hasOwnProperty(multipartSourcePaths[0]) || + (sourceObj[multipartSourcePaths[0]] && + typeof sourceObj[multipartSourcePaths[0]] === 'object')) + ); +} + +function defaultIsDestinationPathOnSource( + sourceObj: unknown, + sourcePath: string +): boolean { + return !!sourceObj.hasOwnProperty(sourcePath); +} + +interface CreateInitialMappingOptions { + isMultipartSourcePathsInSource?: ( + multipartSourcePaths: string[], + sourceObj: unknown + ) => boolean; + isDestinationPathOnSource?: ( + sourceObj: unknown, + sourcePath: string + ) => boolean; + prePropertiesLoop?: (mapping: Mapping) => void; +} + +export function createInitialMapping( + sourceObj: unknown, + destinationObj: unknown, + sourceNestedMetadataMap: unknown[], + destinationNestedMetadataMap: unknown[], + saveMapping: (mapping: Mapping) => void, + options?: CreateMapOptions, + createInitialMappingOptions?: CreateInitialMappingOptions +): Mapping { + const { + isMultipartSourcePathsInSource = defaultIsMultipartSourcePathsInSource, + isDestinationPathOnSource = defaultIsDestinationPathOnSource, + prePropertiesLoop, + } = createInitialMappingOptions ?? {}; + const { extends: bases = [], namingConventions: conventions } = options ?? {}; + + const mapping: Mapping = [ + [sourceObj, destinationObj], + [], + [], + getNamingConventionsFromOptions(conventions), + undefined, + ]; + + prePropertiesLoop?.(mapping); + + const destinationPaths = getPathRecursive(destinationObj); + const namingConventions = mapping[MappingClassId.namingConventions]; + let i = destinationPaths.length; + + while (i--) { + const destinationPath = destinationPaths[i]; + const destinationNestedMetadataAtPath = getNestedMetaKeyAtDestinationPath( + destinationNestedMetadataMap, + sourceNestedMetadataMap, + destinationPath, + namingConventions + ); + + const sourcePath = getSourcePropertyPath( + destinationPath, + namingConventions + ); + + const dottedSourcePaths = sourcePath.split('.'); + if (!isMultipartSourcePathsInSource(dottedSourcePaths, sourceObj)) { + continue; + } + + // With namingConventions, flattening can happen + if ( + !isDestinationPathOnSource(sourceObj, sourcePath) && + isDefined(namingConventions) + ) { + const sourcePaths = getFlatteningSourcePaths( + sourceObj, + sourcePath, + namingConventions + ); + + if (!isDefined(sourcePaths)) { + continue; + } + + mapping[MappingClassId.properties].push([ + destinationPath, + Object.freeze(([ + [destinationPath], + [mapInitialize(...sourcePaths)], + ] as unknown) as MappingProperty), + destinationNestedMetadataAtPath, + ]); + continue; + } + + // Skip if there's no destinationPath on source + if (!isDestinationPathOnSource(sourceObj, sourcePath)) { + continue; + } + + mapping[MappingClassId.properties].push([ + destinationPath, + Object.freeze(([ + [destinationPath, sourcePath], + [mapInitialize(sourcePath)], + ] as unknown) as MappingProperty), + destinationNestedMetadataAtPath, + ]); + } + + // Inherit base mappings + extendMappings(bases, mapping); + + saveMapping(mapping); + + return mapping; +} diff --git a/packages/core/src/lib/utils/extend-mappings.util.ts b/packages/core/src/lib/utils/extend-mappings.util.ts new file mode 100644 index 000000000..b238d4c44 --- /dev/null +++ b/packages/core/src/lib/utils/extend-mappings.util.ts @@ -0,0 +1,29 @@ +import type { Mapping, MappingProperty } from '@automapper/types'; +import { MappingClassId, MappingPropertiesClassId } from '@automapper/types'; + +export function extendMappings(bases: unknown[], mapping: Mapping) { + for (const mappingToExtend of bases) { + const propsToExtend = mappingToExtend[MappingClassId.properties]; + for (let i = 0, len = propsToExtend.length; i < len; i++) { + const [propToExtendKey, propToExtendMappingProp] = propsToExtend[i]; + const existProp = mapping[MappingClassId.properties].find( + ([pKey]) => pKey === propToExtendKey + ); + if (existProp) { + existProp[MappingPropertiesClassId.path] = propToExtendKey; + existProp[MappingPropertiesClassId.property] = Object.freeze( + propToExtendMappingProp + ) as MappingProperty; + } else { + mapping[MappingClassId.properties].push([ + propToExtendKey, + Object.freeze(propToExtendMappingProp) as MappingProperty, + ]); + } + } + mapping[MappingClassId.bases] ??= []; + mapping[MappingClassId.bases].push( + mappingToExtend[MappingClassId.mappings] + ); + } +} diff --git a/packages/core/src/lib/utils/get-flattening-source-paths.util.ts b/packages/core/src/lib/utils/get-flattening-source-paths.util.ts new file mode 100644 index 000000000..4e3f5823a --- /dev/null +++ b/packages/core/src/lib/utils/get-flattening-source-paths.util.ts @@ -0,0 +1,35 @@ +import type { NamingConvention } from '@automapper/types'; + +export function getFlatteningSourcePaths( + src: unknown, + srcPath: string, + namingConventions: [NamingConvention, NamingConvention] +) { + const [sourceNamingConvention] = namingConventions; + const [first, ...paths] = srcPath + .split(sourceNamingConvention.splittingExpression) + .filter(Boolean) + .filter((p) => p !== '.'); + + if (!paths.length || !src.hasOwnProperty(first)) { + return; + } + + const sourcePaths = [ + [first] + .concat( + paths.map((p) => sourceNamingConvention.transformPropertyName([p])) + ) + .join('.'), + ]; + + if (paths.length > 1) { + sourcePaths.push( + [first] + .concat(sourceNamingConvention.transformPropertyName(paths)) + .join('.') + ); + } + + return sourcePaths; +} diff --git a/packages/core/src/lib/utils/get-member-path.util.ts b/packages/core/src/lib/utils/get-member-path.util.ts new file mode 100644 index 000000000..ec36d6e7d --- /dev/null +++ b/packages/core/src/lib/utils/get-member-path.util.ts @@ -0,0 +1,39 @@ +import type { Selector } from '@automapper/types'; + +// TODO: support odd properties like: 'odd-property', 'odd.property'? +export function getMemberPath(fn: Selector): string { + const fnString = fn.toString(); + + // ES6 prop selector: + // "x => x.prop" + if (fnString.includes('=>')) { + const cleaned = cleanseAssertionOperators( + fnString.substring(fnString.indexOf('.') + 1) + ); + + if (cleaned.includes('=>')) return ''; + return cleaned; + } + + // ES5 prop selector: + // "function (x) { return x.prop; }" + // webpack production build excludes the spaces and optional trailing semicolon: + // "function(x){return x.prop}" + // FYI - during local dev testing i observed carriage returns after the curly brackets as well + // Note by maintainer: See https://github.com/IRCraziestTaxi/ts-simple-nameof/pull/13#issuecomment-567171802 for + // explanation of this regex. + const matchRegex = /function\s*\(\w+\)\s*{[\r\n\s]*return\s+\w+\.((\w+\.)*(\w+))/i; + + const es5Match = fnString.match(matchRegex); + + if (es5Match) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return es5Match[1]!; + } + + return ''; +} + +function cleanseAssertionOperators(parsedName: string): string { + return parsedName.replace(/[?!]/g, '').replace(/(?:\s|;|{|}|\(|\)|)+/gm, ''); +} diff --git a/packages/core/src/lib/utils/get-naming-conventions-from-options.util.ts b/packages/core/src/lib/utils/get-naming-conventions-from-options.util.ts new file mode 100644 index 000000000..04b987ed7 --- /dev/null +++ b/packages/core/src/lib/utils/get-naming-conventions-from-options.util.ts @@ -0,0 +1,9 @@ +import type { NamingConvention } from '@automapper/types'; + +export function getNamingConventionsFromOptions(namingConventions?: { + source: NamingConvention; + destination: NamingConvention; +}): [NamingConvention, NamingConvention] | undefined { + if (namingConventions == null) return undefined; + return [namingConventions.source, namingConventions.destination]; +} diff --git a/packages/core/src/lib/utils/get-nested-metakey-at-destination-path.util.ts b/packages/core/src/lib/utils/get-nested-metakey-at-destination-path.util.ts new file mode 100644 index 000000000..0a95f1c1d --- /dev/null +++ b/packages/core/src/lib/utils/get-nested-metakey-at-destination-path.util.ts @@ -0,0 +1,46 @@ +import type { NamingConvention } from '@automapper/types'; +import { isDefined } from './is-defined.util'; + +export function getNestedMetaKeyAtDestinationPath( + destinationNestedMeta: unknown[], + sourceNestedMeta: unknown[], + destinationPath: string, + namingConventions: [NamingConvention, NamingConvention] +) { + let destinationNestedMetaKeyAtPath: [unknown, unknown]; + + if (destinationNestedMeta.length && sourceNestedMeta.length) { + let sourceNestedMetaAtPath; + const destinationNestedMetaAtPath = destinationNestedMeta.find( + ([dnmPath]) => dnmPath === destinationPath + )?.[1]; + + if (isDefined(namingConventions)) { + const [ + sourceNamingConvention, + destinationNamingConvention, + ] = namingConventions; + sourceNestedMetaAtPath = sourceNestedMeta.find( + ([snmPath]) => + destinationNamingConvention.transformPropertyName( + snmPath + .split(sourceNamingConvention.splittingExpression) + .filter(Boolean) + ) === destinationPath + )?.[1]; + } else { + sourceNestedMetaAtPath = sourceNestedMeta.find( + ([snmPath]) => snmPath === destinationPath + )?.[1]; + } + + if (sourceNestedMetaAtPath && destinationNestedMetaAtPath) { + destinationNestedMetaKeyAtPath = [ + destinationNestedMetaAtPath, + sourceNestedMetaAtPath, + ]; + } + } + + return destinationNestedMetaKeyAtPath; +} diff --git a/packages/core/src/lib/utils/get-path-recursive.util.ts b/packages/core/src/lib/utils/get-path-recursive.util.ts new file mode 100644 index 000000000..deba9d6d1 --- /dev/null +++ b/packages/core/src/lib/utils/get-path-recursive.util.ts @@ -0,0 +1,32 @@ +export function getPathRecursive( + node: unknown, + prefix = '', + prev: string[] = [] +): string[] { + const result = prev; + + const keys = Object.getOwnPropertyNames(node); + for (let i = 0, len = keys.length; i < len; i++) { + const key = keys[i]; + const path = prefix + key; + result.push(path); + + const child = node[key]; + if (typeof child === 'object') { + let queue = [child]; + if (Array.isArray(child)) { + queue = child; + } + + for (const childNode of queue) { + const childPaths = getPathRecursive(childNode, path + '.'); + for (const childPath of childPaths) { + if (result.includes(childPath)) continue; + result.push(childPath); + } + } + } + } + + return result; +} diff --git a/packages/core/src/lib/utils/get-source-property-path.util.ts b/packages/core/src/lib/utils/get-source-property-path.util.ts new file mode 100644 index 000000000..193f9b2a9 --- /dev/null +++ b/packages/core/src/lib/utils/get-source-property-path.util.ts @@ -0,0 +1,30 @@ +import type { NamingConvention } from '@automapper/types'; +import { isDefined } from './is-defined.util'; + +export function getSourcePropertyPath( + path: string, + namingConventions?: Readonly<[NamingConvention, NamingConvention]> +): string { + if (!isDefined(namingConventions)) { + return path; + } + + const [ + sourceNamingConvention, + destinationNamingConvention, + ] = namingConventions; + + const splitPath = path.split('.'); + if (splitPath.length > 1) { + return splitPath + .map((key) => getSourcePropertyPath(key, namingConventions)) + .join('.'); + } + + const keyParts = path + .split(destinationNamingConvention.splittingExpression) + .filter(Boolean); + return !keyParts.length + ? path + : sourceNamingConvention.transformPropertyName(keyParts); +} diff --git a/packages/core/src/lib/utils/get.ts b/packages/core/src/lib/utils/get.util.ts similarity index 61% rename from packages/core/src/lib/utils/get.ts rename to packages/core/src/lib/utils/get.util.ts index 9828b9f79..ed21b6e06 100644 --- a/packages/core/src/lib/utils/get.ts +++ b/packages/core/src/lib/utils/get.util.ts @@ -1,12 +1,11 @@ -export function get( - object: T, - defaultVal: unknown, - ...paths: string[] -): unknown { +export function get(object: T, ...paths: string[]): unknown { + if (!paths?.length) { + return; + } + function _getInternal(object: T, path: string) { const _path = path.split('.').filter(Boolean); - const _val = _path.reduce((obj: unknown, key) => obj && obj[key], object); - return _val != null ? _val : defaultVal; + return _path.reduce((obj: unknown, key) => obj && obj[key], object); } let val = _getInternal(object, paths[0]); diff --git a/packages/core/src/lib/utils/index.ts b/packages/core/src/lib/utils/index.ts index a34bde446..a79f5f950 100644 --- a/packages/core/src/lib/utils/index.ts +++ b/packages/core/src/lib/utils/index.ts @@ -1 +1,15 @@ -export * from './get'; +export * from './set.util'; + +export * from './get.util'; +export * from './get-path-recursive.util'; +export * from './get-source-property-path.util'; +export * from './get-naming-conventions-from-options.util'; +export * from './get-member-path.util'; +export * from './get-flattening-source-paths.util'; +export * from './get-nested-metakey-at-destination-path.util'; + +export * from './is-empty.util'; +export * from './is-defined.util'; + +export * from './extend-mappings.util'; +export * from './create-initial-mapping.util'; diff --git a/packages/core/src/lib/utils/is-defined.util.ts b/packages/core/src/lib/utils/is-defined.util.ts new file mode 100644 index 000000000..120379c7f --- /dev/null +++ b/packages/core/src/lib/utils/is-defined.util.ts @@ -0,0 +1,3 @@ +export function isDefined(value: unknown): boolean { + return value != null; +} diff --git a/packages/core/src/lib/utils/is-empty.util.ts b/packages/core/src/lib/utils/is-empty.util.ts new file mode 100644 index 000000000..b2ed847f4 --- /dev/null +++ b/packages/core/src/lib/utils/is-empty.util.ts @@ -0,0 +1,11 @@ +export function isEmpty(value: unknown): boolean { + if (Array.isArray(value)) { + return !value.length; + } + + if (typeof value !== 'object' && typeof value !== 'function') { + return !value; + } + + return !Object.keys(value).length; +} diff --git a/packages/core/src/lib/utils/set.util.ts b/packages/core/src/lib/utils/set.util.ts new file mode 100644 index 000000000..c095a0f2b --- /dev/null +++ b/packages/core/src/lib/utils/set.util.ts @@ -0,0 +1,25 @@ +export function set( + object: T, + path: string, + value: unknown +): (T & { [p: string]: unknown }) | T { + const decomposedPath = path.split('.'); + const base = decomposedPath[0]; + + if (base === undefined) { + return object; + } + + // assign an empty object in order to spread object + if (!object.hasOwnProperty(base)) { + object[base] = {}; + } + + // Determine if there is still layers to traverse + value = + decomposedPath.length <= 1 + ? value + : set(object[base], decomposedPath.slice(1).join('.'), value); + + return Object.assign(object, { [base]: value }); +} diff --git a/packages/core/src/lib/utils/specs/get-member-path.spec.ts b/packages/core/src/lib/utils/specs/get-member-path.spec.ts new file mode 100644 index 000000000..c09689d2e --- /dev/null +++ b/packages/core/src/lib/utils/specs/get-member-path.spec.ts @@ -0,0 +1,69 @@ +import { getMemberPath } from '@automapper/core'; + +describe('getMemberPath', () => { + interface Foo { + foo: string; + bar: { + baz: string; + }; + returnFoo: string; + 'odd-property': string; + } + + it('should return properly for ES6 arrow syntax', () => { + const path = getMemberPath((s: Foo) => s.foo); + expect(path).toEqual('foo'); + }); + + it('should return properly for nested path for ES6 arrow syntax', () => { + const path = getMemberPath((s: Foo) => s.bar.baz); + expect(path).toEqual('bar.baz'); + }); + + it('should return properly for ES5 function syntax', () => { + const path = getMemberPath(function (s: Foo) { + return s.foo; + }); + expect(path).toEqual('foo'); + }); + + it('should return properly for nested path for ES5 function syntax', () => { + const path = getMemberPath(function (s: Foo) { + return s.bar.baz; + }); + expect(path).toEqual('bar.baz'); + }); + + it('should return properly for mixed', () => { + const path = getMemberPath((s: Foo) => { + return s.foo; + }); + expect(path).toEqual('foo'); + }); + + it('should return properly for properties with return keyword', () => { + let path = getMemberPath((s: Foo) => s.returnFoo); + expect(path).toEqual('returnFoo'); + + path = getMemberPath((s: Foo) => { + return s.returnFoo; + }); + expect(path).toEqual('returnFoo'); + + path = getMemberPath(function (s: Foo) { + return s.returnFoo; + }); + expect(path).toEqual('returnFoo'); + }); + + // TODO: need to implement this + it('should return empty string with odd property', () => { + let path = getMemberPath((s: Foo) => s['odd-property']); + expect(path).toEqual(''); + + path = getMemberPath(function (s: Foo) { + return s['odd-property']; + }); + expect(path).toEqual(''); + }); +}); diff --git a/packages/core/src/lib/utils/specs/get-naming-conventions-from-options.spec.ts b/packages/core/src/lib/utils/specs/get-naming-conventions-from-options.spec.ts new file mode 100644 index 000000000..31ff76018 --- /dev/null +++ b/packages/core/src/lib/utils/specs/get-naming-conventions-from-options.spec.ts @@ -0,0 +1,18 @@ +import { + CamelCaseNamingConvention, + getNamingConventionsFromOptions, +} from '@automapper/core'; + +describe('getNamingConventionsFromOptions', () => { + it('should return properly', () => { + expect(getNamingConventionsFromOptions()).toEqual(undefined); + + const camelCaseNamingConvention = new CamelCaseNamingConvention(); + expect( + getNamingConventionsFromOptions({ + source: camelCaseNamingConvention, + destination: camelCaseNamingConvention, + }) + ).toEqual([camelCaseNamingConvention, camelCaseNamingConvention]); + }); +}); diff --git a/packages/core/src/lib/utils/specs/get-path-recursive.spec.ts b/packages/core/src/lib/utils/specs/get-path-recursive.spec.ts new file mode 100644 index 000000000..985a17d51 --- /dev/null +++ b/packages/core/src/lib/utils/specs/get-path-recursive.spec.ts @@ -0,0 +1,38 @@ +import { getPathRecursive } from '@automapper/core'; + +describe('getPathRecursive', () => { + const node = { + foo: undefined, + bar: { + baz: undefined, + }, + baz: [], + barBaz: [ + { + fooBar: { + barFoo: undefined, + }, + }, + { + fooBar: { + barFoo: undefined, + }, + }, + ], + }; + + const resultPaths = [ + 'foo', + 'bar', + 'bar.baz', + 'baz', + 'barBaz', + 'barBaz.fooBar', + 'barBaz.fooBar.barFoo', + ]; + + it('should work', () => { + const paths = getPathRecursive(node); + expect(paths).toEqual(resultPaths); + }); +}); diff --git a/packages/core/src/lib/utils/specs/get-source-property-path.spec.ts b/packages/core/src/lib/utils/specs/get-source-property-path.spec.ts new file mode 100644 index 000000000..95b857bd6 --- /dev/null +++ b/packages/core/src/lib/utils/specs/get-source-property-path.spec.ts @@ -0,0 +1,31 @@ +import { + CamelCaseNamingConvention, + getSourcePropertyPath, + PascalCaseNamingConvention, +} from '@automapper/core'; + +describe('getSourcePropertyPath', () => { + const camelPascalNamingConventions = [ + new CamelCaseNamingConvention(), + new PascalCaseNamingConvention(), + ] as const; + + it('should return path as-is if namingConventions are not provided', () => { + const sourcePath = getSourcePropertyPath('foo.bar'); + expect(sourcePath).toEqual('foo.bar'); + }); + + it('should return path with namingConventions', () => { + let sourcePath = getSourcePropertyPath( + 'Foo.Bar', + camelPascalNamingConventions + ); + expect(sourcePath).toEqual('foo.bar'); + + sourcePath = getSourcePropertyPath( + 'FooBarBaz', + camelPascalNamingConventions + ); + expect(sourcePath).toEqual('fooBarBaz'); + }); +}); diff --git a/packages/core/src/lib/utils/specs/get.spec.ts b/packages/core/src/lib/utils/specs/get.spec.ts new file mode 100644 index 000000000..91386602f --- /dev/null +++ b/packages/core/src/lib/utils/specs/get.spec.ts @@ -0,0 +1,30 @@ +import { get } from '@automapper/core'; + +describe('get', () => { + const obj = { foo: { bar: 'bar' } }; + + it('should return bar', () => { + const result = get(obj, 'foo', 'bar'); + expect(result).toEqual('bar'); + }); + + it('should return null', () => { + const result = get({ foo: { bar: null } }, 'foo', 'bar'); + expect(result).toEqual(null); + }); + + it('should return undefined for unknown path', () => { + const result = get(obj, 'foo', 'baz'); + expect(result).toEqual(undefined); + }); + + it('should return object', () => { + const result = get(obj, 'foo'); + expect(result).toEqual({ bar: 'bar' }); + }); + + it('should return undefined if paths are not provided', () => { + const result = get(obj); + expect(result).toEqual(undefined); + }); +}); diff --git a/packages/core/src/lib/utils/specs/is-defined.spec.ts b/packages/core/src/lib/utils/specs/is-defined.spec.ts new file mode 100644 index 000000000..e026d2c39 --- /dev/null +++ b/packages/core/src/lib/utils/specs/is-defined.spec.ts @@ -0,0 +1,14 @@ +import { isDefined } from '@automapper/core'; + +describe('isDefined', () => { + it('should return properly', () => { + expect(isDefined(null)).toEqual(false); + expect(isDefined(undefined)).toEqual(false); + expect(isDefined('foo')).toEqual(true); + expect(isDefined(String)).toEqual(true); + expect(isDefined([])).toEqual(true); + expect(isDefined({})).toEqual(true); + expect(isDefined(0)).toEqual(true); + expect(isDefined('')).toEqual(true); + }); +}); diff --git a/packages/core/src/lib/utils/specs/is-empty.spec.ts b/packages/core/src/lib/utils/specs/is-empty.spec.ts new file mode 100644 index 000000000..383d498d0 --- /dev/null +++ b/packages/core/src/lib/utils/specs/is-empty.spec.ts @@ -0,0 +1,17 @@ +import { isEmpty } from '@automapper/core'; + +describe('isEmpty', () => { + it('should return properly', () => { + expect(isEmpty([])).toEqual(true); + expect(isEmpty({})).toEqual(true); + expect(isEmpty('')).toEqual(true); + expect(isEmpty(0)).toEqual(true); + expect(isEmpty(false)).toEqual(true); + + expect(isEmpty([''])).toEqual(false); + expect(isEmpty({ foo: '' })).toEqual(false); + expect(isEmpty('123')).toEqual(false); + expect(isEmpty(123)).toEqual(false); + expect(isEmpty(true)).toEqual(false); + }); +}); diff --git a/packages/core/src/lib/utils/specs/set.spec.ts b/packages/core/src/lib/utils/specs/set.spec.ts new file mode 100644 index 000000000..3f42d42ff --- /dev/null +++ b/packages/core/src/lib/utils/specs/set.spec.ts @@ -0,0 +1,21 @@ +import { set } from '@automapper/core'; + +describe('set', () => { + it('should set nested property', () => { + const result = set({ foo: { bar: 'foo' } }, 'foo.bar', 'baz'); + expect(result).toEqual({ foo: { bar: 'baz' } }); + }); + + it('should set obj', () => { + const result = set({ foo: { bar: 'foo' } }, 'foo', { baz: 'foo' }); + expect(result).toEqual({ foo: { baz: 'foo' } }); + }); + + it('should add property to obj at unknown path', () => { + let result = set({ foo: { bar: 'foo' } }, 'bar', 'foo'); + expect(result).toEqual({ foo: { bar: 'foo' }, bar: 'foo' }); + + result = set({ foo: { bar: 'foo' } }, 'foo.baz', 'baz'); + expect(result).toEqual({ foo: { bar: 'foo', baz: 'baz' } }); + }); +}); diff --git a/packages/models/.eslintrc.json b/packages/integration-test/.eslintrc.json similarity index 100% rename from packages/models/.eslintrc.json rename to packages/integration-test/.eslintrc.json diff --git a/packages/integration-test/README.md b/packages/integration-test/README.md new file mode 100644 index 000000000..427e03e6b --- /dev/null +++ b/packages/integration-test/README.md @@ -0,0 +1,7 @@ +# integration-test + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test integration-test` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/integration-test/jest-setup.ts b/packages/integration-test/jest-setup.ts new file mode 100644 index 000000000..d2c9bc6e6 --- /dev/null +++ b/packages/integration-test/jest-setup.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; diff --git a/packages/integration-test/jest.config.js b/packages/integration-test/jest.config.js new file mode 100644 index 000000000..afdb8fcb0 --- /dev/null +++ b/packages/integration-test/jest.config.js @@ -0,0 +1,18 @@ +// eslint-disable-next-line no-undef +module.exports = { + displayName: 'integration-test', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/packages/integration-test', + setupFiles: ['./jest-setup.ts'], + testPathIgnorePatterns: ['../../node_modules', 'src/lib/setup.spec.ts'], +}; diff --git a/packages/integration-test/src/index.ts b/packages/integration-test/src/index.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/integration-test/src/index.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/integration-test/src/lib/setup.spec.ts b/packages/integration-test/src/lib/setup.spec.ts new file mode 100644 index 000000000..6065af667 --- /dev/null +++ b/packages/integration-test/src/lib/setup.spec.ts @@ -0,0 +1,39 @@ +import { classes } from '@automapper/classes'; +import { createMapper } from '@automapper/core'; +import { pojos } from '@automapper/pojos'; +import type { CreateMapperOptions, Mapper } from '@automapper/types'; + +export function setup( + name: string, + pluginInitializer: CreateMapperOptions['pluginInitializer'], + namingConventions?: CreateMapperOptions['namingConventions'] +): [Mapper, jest.Mock] { + const spiedErrorHandle = jest.fn(); + const mapper: Mapper = createMapper({ + name, + pluginInitializer, + namingConventions, + errorHandle: { handle: spiedErrorHandle }, + }); + + afterEach(() => { + mapper?.dispose(); + spiedErrorHandle.mockReset(); + }); + + return [mapper, spiedErrorHandle]; +} + +export function setupClasses( + name: string, + namingConventions?: CreateMapperOptions['namingConventions'] +): [Mapper, jest.Mock] { + return setup(name, classes, namingConventions); +} + +export function setupPojos( + name: string, + namingConventions?: CreateMapperOptions['namingConventions'] +): [Mapper, jest.Mock] { + return setup(name, pojos, namingConventions); +} diff --git a/packages/integration-test/src/lib/with-classes/circular-deps.spec.ts b/packages/integration-test/src/lib/with-classes/circular-deps.spec.ts new file mode 100644 index 000000000..945a7cc3c --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/circular-deps.spec.ts @@ -0,0 +1,44 @@ +import { setupClasses } from '../setup.spec'; +import { + BarWithFoo, + BarWithFooZeroDepth, +} from './fixtures/models/circular-deps-bar'; +import { + FooWithBar, + FooWithBarZeroDepth, +} from './fixtures/models/circular-deps-foo'; + +describe('Circular Dependencies', () => { + const [mapper] = setupClasses('circularDeps'); + + it('should map with depth of 1', () => { + mapper.createMap(BarWithFoo, BarWithFoo); + mapper.createMap(FooWithBar, FooWithBar); + + const vm = mapper.map( + { id: '1', bar: { foo: { id: '2', bar: null }, id: '1' } }, + FooWithBar, + FooWithBar + ); + + expect(vm).toBeTruthy(); + expect(vm.id).toEqual('1'); + expect(vm.bar).toBeInstanceOf(BarWithFoo); + expect(vm.bar?.foo).toBeInstanceOf(FooWithBar); + }); + + it('should map with depth of 0', () => { + mapper.createMap(BarWithFooZeroDepth, BarWithFooZeroDepth); + mapper.createMap(FooWithBarZeroDepth, FooWithBarZeroDepth); + + const vm = mapper.map( + { id: '1', bar: { foo: { id: '2', bar: null }, id: '1' } }, + FooWithBarZeroDepth, + FooWithBarZeroDepth + ); + expect(vm).toBeTruthy(); + expect(vm.id).toEqual('1'); + expect(vm.bar).toBeInstanceOf(BarWithFooZeroDepth); + expect(vm.bar?.foo).toBeInstanceOf(FooWithBarZeroDepth); + }); +}); diff --git a/packages/integration-test/src/lib/with-classes/creation.spec.ts b/packages/integration-test/src/lib/with-classes/creation.spec.ts new file mode 100644 index 000000000..65645afd7 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/creation.spec.ts @@ -0,0 +1,52 @@ +import { MappingProfile } from '@automapper/types'; +import { setupClasses } from '../setup.spec'; +import { + SimpleBar, + SimpleBarVm, + SimpleFoo, + SimpleFooVm, +} from './fixtures/models/simple-foo-bar'; + +describe('Creation', () => { + const [mapper, spiedErrorHandler] = setupClasses('creation'); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should create mapping', () => { + expect(mapper.getMapping(SimpleBar, SimpleBarVm)).toBeUndefined(); + expect(mapper.getMapping(SimpleFoo, SimpleFooVm)).toBeUndefined(); + + expect(spiedErrorHandler).toHaveBeenCalledTimes(2); + + mapper.createMap(SimpleBar, SimpleBarVm); + mapper.createMap(SimpleFoo, SimpleFooVm); + + expect(mapper.getMapping(SimpleBar, SimpleBarVm)).toBeTruthy(); + expect(mapper.getMapping(SimpleFoo, SimpleFooVm)).toBeTruthy(); + }); + + it('should throw when creating duplicate mapping', () => { + mapper.createMap(SimpleBar, SimpleBarVm); + expect(mapper.getMapping(SimpleBar, SimpleBarVm)).toBeTruthy(); + + mapper.createMap(SimpleBar, SimpleBarVm); + expect(spiedErrorHandler).toHaveBeenNthCalledWith( + 1, + `Mapping for source ${SimpleBar.toString()} and destination ${SimpleBarVm.toString()} already exists` + ); + }); + + it('should add profile', () => { + const profile: MappingProfile = (_mapper) => { + _mapper.createMap(SimpleBar, SimpleBarVm); + _mapper.createMap(SimpleFoo, SimpleFooVm); + }; + + mapper.addProfile(profile); + + expect(mapper.getMapping(SimpleBar, SimpleBarVm)).toBeTruthy(); + expect(mapper.getMapping(SimpleFoo, SimpleFooVm)).toBeTruthy(); + }); +}); diff --git a/packages/integration-test/src/lib/with-classes/defer.spec.ts b/packages/integration-test/src/lib/with-classes/defer.spec.ts new file mode 100644 index 000000000..e66b82768 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/defer.spec.ts @@ -0,0 +1,40 @@ +import { mapDefer, mapFrom, preCondition } from '@automapper/core'; +import { setupClasses } from '../setup.spec'; +import { SimpleUser, SimpleUserVm } from './fixtures/models/simple-user'; + +describe('Defer', () => { + const [mapper] = setupClasses('defer'); + + it('should map correctly', () => { + mapper.createMap(SimpleUser, SimpleUserVm).forMember( + (d) => d.fullName, + mapDefer(() => mapFrom((s) => s.firstName + ' ' + s.lastName)) + ); + + const user = new SimpleUser(); + user.firstName = 'Chau'; + user.lastName = 'Tran'; + + const vm = mapper.map(user, SimpleUserVm, SimpleUser); + expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); + }); + + it('should map correctly with preCond', () => { + mapper.createMap(SimpleUser, SimpleUserVm).forMember( + (d) => d.fullName, + preCondition((s) => s.firstName === 'Phuong'), + mapDefer(() => mapFrom((s) => s.firstName + ' ' + s.lastName)) + ); + + const user = new SimpleUser(); + user.firstName = 'Chau'; + user.lastName = 'Tran'; + + let vm = mapper.map(user, SimpleUserVm, SimpleUser); + expect(vm.fullName).toEqual(undefined); + + user.firstName = 'Phuong'; + vm = mapper.map(user, SimpleUserVm, SimpleUser); + expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); + }); +}); diff --git a/packages/integration-test/src/lib/with-classes/fixtures/converters/date-to-string.converter.ts b/packages/integration-test/src/lib/with-classes/fixtures/converters/date-to-string.converter.ts new file mode 100644 index 000000000..7dbacb430 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/converters/date-to-string.converter.ts @@ -0,0 +1,7 @@ +import type { Converter } from '@automapper/types'; + +export const dateToStringConverter: Converter = { + convert(source: Date): string { + return source.toDateString(); + }, +}; diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/address-pascal.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/address-pascal.ts new file mode 100644 index 000000000..53d5ef0c4 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/address-pascal.ts @@ -0,0 +1,15 @@ +import { AutoMap } from '@automapper/classes'; + +export class PascalAddress { + @AutoMap() + Street: string; + @AutoMap() + City: string; + @AutoMap() + State: string; +} + +export class PascalAddressVm { + @AutoMap() + FormattedAddress: string; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/address-snake.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/address-snake.ts new file mode 100644 index 000000000..e4c8cace9 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/address-snake.ts @@ -0,0 +1,15 @@ +import { AutoMap } from '@automapper/classes'; + +export class SnakeAddress { + @AutoMap() + street: string; + @AutoMap() + city: string; + @AutoMap() + state: string; +} + +export class SnakeAddressVm { + @AutoMap() + formatted_address: string; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/address.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/address.ts new file mode 100644 index 000000000..f82d09158 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/address.ts @@ -0,0 +1,15 @@ +import { AutoMap } from '@automapper/classes'; + +export class Address { + @AutoMap() + street: string; + @AutoMap() + city: string; + @AutoMap() + state: string; +} + +export class AddressVm { + @AutoMap() + formattedAddress: string; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/avatar-pascal.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/avatar-pascal.ts new file mode 100644 index 000000000..01a093996 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/avatar-pascal.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; + +export class PascalAvatar { + @AutoMap() + Url: string; + @AutoMap() + Source: string; + @AutoMap() + ShouldIgnore: number; + @AutoMap() + ShouldBeSubstituted: string; + @AutoMap() + ForCondition: boolean; +} + +export class PascalAvatarVm { + @AutoMap() + Url: string; + @AutoMap() + WillBeIgnored: number; + @AutoMap() + ShouldBeSubstituted: string; + @AutoMap() + ForCondition: boolean; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/avatar-snake.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/avatar-snake.ts new file mode 100644 index 000000000..9bd0e05be --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/avatar-snake.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; + +export class SnakeAvatar { + @AutoMap() + url: string; + @AutoMap() + source: string; + @AutoMap() + should_ignore: number; + @AutoMap() + should_be_substituted: string; + @AutoMap() + for_condition: boolean; +} + +export class SnakeAvatarVm { + @AutoMap() + url: string; + @AutoMap() + will_be_ignored: number; + @AutoMap() + should_be_substituted: string; + @AutoMap() + for_condition: boolean; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/avatar.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/avatar.ts new file mode 100644 index 000000000..5cb16f791 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/avatar.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; + +export class Avatar { + @AutoMap() + url: string; + @AutoMap() + source: string; + @AutoMap() + shouldIgnore: number; + @AutoMap() + shouldBeSubstituted: string; + @AutoMap() + forCondition: boolean; +} + +export class AvatarVm { + @AutoMap() + url: string; + @AutoMap() + willBeIgnored: number; + @AutoMap() + shouldBeSubstituted: string; + @AutoMap() + forCondition: boolean; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/circular-deps-bar.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/circular-deps-bar.ts new file mode 100644 index 000000000..ba5b5fe25 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/circular-deps-bar.ts @@ -0,0 +1,16 @@ +import { AutoMap } from '@automapper/classes'; +import { FooWithBar, FooWithBarZeroDepth } from './circular-deps-foo'; + +export class BarWithFoo { + @AutoMap() + id: string; + @AutoMap(() => FooWithBar, 1) + foo: FooWithBar; +} + +export class BarWithFooZeroDepth { + @AutoMap() + id: string; + @AutoMap(() => FooWithBarZeroDepth) + foo: FooWithBarZeroDepth; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/circular-deps-foo.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/circular-deps-foo.ts new file mode 100644 index 000000000..af21ae7a0 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/circular-deps-foo.ts @@ -0,0 +1,16 @@ +import { AutoMap } from '@automapper/classes'; +import { BarWithFoo, BarWithFooZeroDepth } from './circular-deps-bar'; + +export class FooWithBar { + @AutoMap() + id: string; + @AutoMap(() => BarWithFoo, 1) + bar: BarWithFoo | null; +} + +export class FooWithBarZeroDepth { + @AutoMap() + id: string; + @AutoMap(() => BarWithFooZeroDepth) + bar: BarWithFooZeroDepth | null; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/job-pascal.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/job-pascal.ts new file mode 100644 index 000000000..7f975b0d2 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/job-pascal.ts @@ -0,0 +1,8 @@ +import { AutoMap } from '@automapper/classes'; + +export class PascalJob { + @AutoMap() + Title: string; + @AutoMap() + AnnualSalary: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/job-snake.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/job-snake.ts new file mode 100644 index 000000000..820b5fe52 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/job-snake.ts @@ -0,0 +1,8 @@ +import { AutoMap } from '@automapper/classes'; + +export class SnakeJob { + @AutoMap() + title: string; + @AutoMap() + annual_salary: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/job.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/job.ts new file mode 100644 index 000000000..74bbe6456 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/job.ts @@ -0,0 +1,8 @@ +import { AutoMap } from '@automapper/classes'; + +export class Job { + @AutoMap() + title: string; + @AutoMap() + annualSalary: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar-pascal.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar-pascal.ts new file mode 100644 index 000000000..fe7288936 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar-pascal.ts @@ -0,0 +1,29 @@ +import { AutoMap } from '@automapper/classes'; + +export class PascalSimpleBar { + @AutoMap() + Bar: string; +} + +export class PascalSimpleBarVm { + @AutoMap() + Bar: string; +} + +export class PascalSimpleFoo { + @AutoMap() + Foo: string; + @AutoMap(() => PascalSimpleBar) + Bar: PascalSimpleBar; + @AutoMap() + FooBar: number; +} + +export class PascalSimpleFooVm { + @AutoMap() + Foo: string; + @AutoMap(() => PascalSimpleBarVm) + Bar: PascalSimpleBarVm; + @AutoMap() + FooBar: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar-snake.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar-snake.ts new file mode 100644 index 000000000..0f3397646 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar-snake.ts @@ -0,0 +1,29 @@ +import { AutoMap } from '@automapper/classes'; + +export class SnakeSimpleBar { + @AutoMap() + bar: string; +} + +export class SnakeSimpleBarVm { + @AutoMap() + bar: string; +} + +export class SnakeSimpleFoo { + @AutoMap() + foo: string; + @AutoMap(() => SnakeSimpleBar) + bar: SnakeSimpleBar; + @AutoMap() + foo_bar: number; +} + +export class SnakeSimpleFooVm { + @AutoMap() + foo: string; + @AutoMap(() => SnakeSimpleBarVm) + bar: SnakeSimpleBarVm; + @AutoMap() + foo_bar: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar.ts new file mode 100644 index 000000000..73bfb859d --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-foo-bar.ts @@ -0,0 +1,29 @@ +import { AutoMap } from '@automapper/classes'; + +export class SimpleBar { + @AutoMap() + bar: string; +} + +export class SimpleBarVm { + @AutoMap() + bar: string; +} + +export class SimpleFoo { + @AutoMap() + foo: string; + @AutoMap(() => SimpleBar) + bar: SimpleBar; + @AutoMap() + fooBar: number; +} + +export class SimpleFooVm { + @AutoMap() + foo: string; + @AutoMap(() => SimpleBarVm) + bar: SimpleBarVm; + @AutoMap() + fooBar: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/simple-inheritance-foo.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-inheritance-foo.ts new file mode 100644 index 000000000..9e66d442d --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-inheritance-foo.ts @@ -0,0 +1,35 @@ +import { AutoMap } from '@automapper/classes'; + +export class Foo { + @AutoMap() + foo: string; +} + +export class FooVm { + @AutoMap() + fooVm: string; +} + +export class FooFoo extends Foo { + @AutoMap() + fooFoo: string; +} + +export class FooFooVm extends FooVm { + @AutoMap() + fooFooVm: string; +} + +export class FooFooFoo extends FooFoo { + @AutoMap() + fooFooFoo: string; +} + +export class FooFooFooVm extends FooFooVm { + @AutoMap() + fooFooFooVm: string; +} + +export class EmptyFoo extends FooFooFoo {} + +export class EmptyFooVm extends FooFooFooVm {} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/simple-user.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-user.ts new file mode 100644 index 000000000..2444b7a9e --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/simple-user.ts @@ -0,0 +1,17 @@ +import { AutoMap } from '@automapper/classes'; + +export class SimpleUser { + @AutoMap() + firstName: string; + @AutoMap() + lastName: string; +} + +export class SimpleUserVm { + @AutoMap() + firstName: string; + @AutoMap() + lastName: string; + @AutoMap() + fullName: string; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/user-pascal.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/user-pascal.ts new file mode 100644 index 000000000..9b98cf6b3 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/user-pascal.ts @@ -0,0 +1,29 @@ +import { AutoMap } from '@automapper/classes'; +import { PascalJob } from './job-pascal'; +import { PascalUserProfile, PascalUserProfileVm } from './user-profile-pascal'; + +export class PascalUser { + @AutoMap() + FirstName: string; + @AutoMap() + LastName: string; + @AutoMap(() => PascalUserProfile) + Profile: PascalUserProfile; + @AutoMap(() => PascalJob) + Job: PascalJob; +} + +export class PascalUserVm { + @AutoMap() + First: string; + @AutoMap() + Last: string; + @AutoMap() + Full: string; + @AutoMap(() => PascalUserProfileVm) + Profile: PascalUserProfileVm; + @AutoMap() + JobTitle: string; + @AutoMap() + JobAnnualSalary: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile-pascal.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile-pascal.ts new file mode 100644 index 000000000..a7a1b9266 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile-pascal.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; +import { PascalAddress, PascalAddressVm } from './address-pascal'; +import { PascalAvatar, PascalAvatarVm } from './avatar-pascal'; + +export class PascalUserProfile { + @AutoMap() + Bio: string; + @AutoMap() + Birthday: Date; + @AutoMap(() => PascalAvatar) + Avatar: PascalAvatar; + @AutoMap(() => PascalAddress) + Addresses: PascalAddress[]; +} + +export class PascalUserProfileVm { + @AutoMap() + Bio: string; + @AutoMap() + Birthday: string; + @AutoMap(() => PascalAvatarVm) + Avatar: PascalAvatarVm; + @AutoMap(() => PascalAddressVm) + Addresses: PascalAddressVm[]; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile-snake.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile-snake.ts new file mode 100644 index 000000000..9f0614791 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile-snake.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; +import { SnakeAddress, SnakeAddressVm } from './address-snake'; +import { SnakeAvatar, SnakeAvatarVm } from './avatar-snake'; + +export class SnakeUserProfile { + @AutoMap() + bio: string; + @AutoMap() + birthday: Date; + @AutoMap(() => SnakeAvatar) + avatar: SnakeAvatar; + @AutoMap(() => SnakeAddress) + addresses: SnakeAddress[]; +} + +export class SnakeUserProfileVm { + @AutoMap() + bio: string; + @AutoMap() + birthday: string; + @AutoMap(() => SnakeAvatarVm) + avatar: SnakeAvatarVm; + @AutoMap(() => SnakeAddressVm) + addresses: SnakeAddressVm[]; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile.ts new file mode 100644 index 000000000..3707eeb94 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/user-profile.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; +import { Address, AddressVm } from './address'; +import { Avatar, AvatarVm } from './avatar'; + +export class UserProfile { + @AutoMap() + bio: string; + @AutoMap() + birthday: Date; + @AutoMap(() => Avatar) + avatar: Avatar; + @AutoMap(() => Address) + addresses: Address[]; +} + +export class UserProfileVm { + @AutoMap() + bio: string; + @AutoMap() + birthday: string; + @AutoMap(() => AvatarVm) + avatar: AvatarVm; + @AutoMap(() => AddressVm) + addresses: AddressVm[]; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/user-snake.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/user-snake.ts new file mode 100644 index 000000000..b5e9d7a39 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/user-snake.ts @@ -0,0 +1,29 @@ +import { AutoMap } from '@automapper/classes'; +import { SnakeJob } from './job-snake'; +import { SnakeUserProfile, SnakeUserProfileVm } from './user-profile-snake'; + +export class SnakeUser { + @AutoMap() + first_name: string; + @AutoMap() + last_name: string; + @AutoMap(() => SnakeUserProfile) + profile: SnakeUserProfile; + @AutoMap(() => SnakeJob) + job: SnakeJob; +} + +export class SnakeUserVm { + @AutoMap() + first: string; + @AutoMap() + last: string; + @AutoMap() + full: string; + @AutoMap(() => SnakeUserProfileVm) + profile: SnakeUserProfileVm; + @AutoMap() + job_title: string; + @AutoMap() + job_annual_salary: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/user-variants.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/user-variants.ts new file mode 100644 index 000000000..aff104e6c --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/user-variants.ts @@ -0,0 +1,39 @@ +import { AutoMap } from '@automapper/classes'; + +export class UserWithGetter { + private _firstName: string; + @AutoMap() + get firstName() { + return this._firstName; + } + + set firstName(value: string) { + this._firstName = value; + } + + private _lastName: string; + @AutoMap() + get lastName() { + return this._lastName; + } + + set lastName(value: string) { + this._lastName = value; + } +} + +export class UserWithReturnKeyword { + @AutoMap() + returnFirstName: string; + @AutoMap() + returnLastName: string; +} + +export class UserWithReturnKeywordVm { + @AutoMap() + returnReturnFirst: string; + @AutoMap() + returnReturnLast: string; + @AutoMap() + returnReturnFull: string; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/user.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/user.ts new file mode 100644 index 000000000..68ca4ba61 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/user.ts @@ -0,0 +1,29 @@ +import { AutoMap } from '@automapper/classes'; +import { Job } from './job'; +import { UserProfile, UserProfileVm } from './user-profile'; + +export class User { + @AutoMap() + firstName: string; + @AutoMap() + lastName: string; + @AutoMap(() => UserProfile) + profile: UserProfile; + @AutoMap(() => Job) + job: Job; +} + +export class UserVm { + @AutoMap() + first: string; + @AutoMap() + last: string; + @AutoMap() + full: string; + @AutoMap(() => UserProfileVm) + profile: UserProfileVm; + @AutoMap() + jobTitle: string; + @AutoMap() + jobAnnualSalary: number; +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/address.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/address.profile.ts new file mode 100644 index 000000000..f4a4c24e9 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/address.profile.ts @@ -0,0 +1,50 @@ +import { mapFrom } from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import { Address, AddressVm } from '../models/address'; +import { PascalAddress, PascalAddressVm } from '../models/address-pascal'; +import { SnakeAddress, SnakeAddressVm } from '../models/address-snake'; + +export const addressProfile: MappingProfile = (mapper) => { + mapper.createMap(Address, AddressVm).forMember( + (d) => d.formattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper.createMap(Address, PascalAddressVm).forMember( + (d) => d.FormattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper.createMap(Address, SnakeAddressVm).forMember( + (d) => d.formatted_address, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); +}; + +export const pascalAddressProfile: MappingProfile = (mapper) => { + mapper.createMap(PascalAddress, PascalAddressVm).forMember( + (d) => d.FormattedAddress, + mapFrom((s) => `${s.Street} ${s.City} ${s.State}`) + ); + mapper.createMap(PascalAddress, AddressVm).forMember( + (d) => d.formattedAddress, + mapFrom((s) => `${s.Street} ${s.City} ${s.State}`) + ); + mapper.createMap(PascalAddress, SnakeAddressVm).forMember( + (d) => d.formatted_address, + mapFrom((s) => `${s.Street} ${s.City} ${s.State}`) + ); +}; + +export const snakeAddressProfile: MappingProfile = (mapper) => { + mapper.createMap(SnakeAddress, SnakeAddressVm).forMember( + (d) => d.formatted_address, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper.createMap(SnakeAddress, AddressVm).forMember( + (d) => d.formattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper.createMap(SnakeAddress, PascalAddressVm).forMember( + (d) => d.FormattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); +}; diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/avatar.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/avatar.profile.ts new file mode 100644 index 000000000..5517cae8f --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/avatar.profile.ts @@ -0,0 +1,146 @@ +import { + condition, + ignore, + mapFrom, + nullSubstitution, + preCondition, +} from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import { Avatar, AvatarVm } from '../models/avatar'; +import { PascalAvatar, PascalAvatarVm } from '../models/avatar-pascal'; +import { SnakeAvatar, SnakeAvatarVm } from '../models/avatar-snake'; + +export const FOR_SHOULD_IGNORE_PASS_CONDITION = 6; +export const FOR_SHOULD_IGNORE_FAIL_CONDITION = 5; + +export const avatarProfile: MappingProfile = (mapper) => { + mapper + .createMap(Avatar, AvatarVm) + .forMember( + (d) => d.url, + preCondition((s) => s.shouldIgnore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.forCondition, + condition((s) => s.shouldIgnore > 5, true) + ) + .forMember((d) => d.willBeIgnored, ignore()) + .forMember((d) => d.shouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap(Avatar, PascalAvatarVm) + .forMember( + (d) => d.Url, + preCondition((s) => s.shouldIgnore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.ForCondition, + condition((s) => s.shouldIgnore > 5, true) + ) + .forMember((d) => d.WillBeIgnored, ignore()) + .forMember((d) => d.ShouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap(Avatar, SnakeAvatarVm) + .forMember( + (d) => d.url, + preCondition((s) => s.shouldIgnore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.for_condition, + condition((s) => s.shouldIgnore > 5, true) + ) + .forMember((d) => d.will_be_ignored, ignore()) + .forMember((d) => d.should_be_substituted, nullSubstitution('sub')); +}; + +export const pascalAvatarProfile: MappingProfile = (mapper) => { + mapper + .createMap(PascalAvatar, PascalAvatarVm) + .forMember( + (d) => d.Url, + preCondition((s) => s.ShouldIgnore > 5, 'default url'), + mapFrom((s) => s.Source) + ) + .forMember( + (d) => d.ForCondition, + condition((s) => s.ShouldIgnore > 5, true) + ) + .forMember((d) => d.WillBeIgnored, ignore()) + .forMember((d) => d.ShouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap(PascalAvatar, AvatarVm) + .forMember( + (d) => d.url, + preCondition((s) => s.ShouldIgnore > 5, 'default url'), + mapFrom((s) => s.Source) + ) + .forMember( + (d) => d.forCondition, + condition((s) => s.ShouldIgnore > 5, true) + ) + .forMember((d) => d.willBeIgnored, ignore()) + .forMember((d) => d.shouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap(PascalAvatar, SnakeAvatarVm) + .forMember( + (d) => d.url, + preCondition((s) => s.ShouldIgnore > 5, 'default url'), + mapFrom((s) => s.Source) + ) + .forMember( + (d) => d.for_condition, + condition((s) => s.ShouldIgnore > 5, true) + ) + .forMember((d) => d.will_be_ignored, ignore()) + .forMember((d) => d.should_be_substituted, nullSubstitution('sub')); +}; + +export const snakeAvatarProfile: MappingProfile = (mapper) => { + mapper + .createMap(SnakeAvatar, SnakeAvatarVm) + .forMember( + (d) => d.url, + preCondition((s) => s.should_ignore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.for_condition, + condition((s) => s.should_ignore > 5, true) + ) + .forMember((d) => d.will_be_ignored, ignore()) + .forMember((d) => d.should_be_substituted, nullSubstitution('sub')); + + mapper + .createMap(SnakeAvatar, AvatarVm) + .forMember( + (d) => d.url, + preCondition((s) => s.should_ignore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.forCondition, + condition((s) => s.should_ignore > 5, true) + ) + .forMember((d) => d.willBeIgnored, ignore()) + .forMember((d) => d.shouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap(SnakeAvatar, PascalAvatarVm) + .forMember( + (d) => d.Url, + preCondition((s) => s.should_ignore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.ForCondition, + condition((s) => s.should_ignore > 5, true) + ) + .forMember((d) => d.WillBeIgnored, ignore()) + .forMember((d) => d.ShouldBeSubstituted, nullSubstitution('sub')); +}; diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/simple-inheritance-foo.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/simple-inheritance-foo.profile.ts new file mode 100644 index 000000000..e060e0374 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/simple-inheritance-foo.profile.ts @@ -0,0 +1,39 @@ +import { mapFrom } from '@automapper/core'; +import { MappingProfile } from '@automapper/types'; +import { + EmptyFoo, + EmptyFooVm, + Foo, + FooFoo, + FooFooFoo, + FooFooFooVm, + FooFooVm, + FooVm, +} from '../models/simple-inheritance-foo'; + +export const simpleInheritanceFooProfile: MappingProfile = (mapper) => { + mapper.createMap(Foo, FooVm).forMember( + (d) => d.fooVm, + mapFrom((s) => s.foo) + ); + + mapper + .createMap(FooFoo, FooFooVm, { extends: [mapper.getMapping(Foo, FooVm)] }) + .forMember( + (d) => d.fooFooVm, + mapFrom((s) => s.fooFoo) + ); + + mapper + .createMap(FooFooFoo, FooFooFooVm, { + extends: [mapper.getMapping(FooFoo, FooFooVm)], + }) + .forMember( + (d) => d.fooFooFooVm, + mapFrom((s) => s.fooFooFoo) + ); + + mapper.createMap(EmptyFoo, EmptyFooVm, { + extends: [mapper.getMapping(FooFooFoo, FooFooFooVm)], + }); +}; diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/simple-user.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/simple-user.profile.ts new file mode 100644 index 000000000..ca89bd440 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/simple-user.profile.ts @@ -0,0 +1,19 @@ +import { mapFrom } from '@automapper/core'; +import type { MapAction, Mapper, MappingProfile } from '@automapper/types'; +import { SimpleUser, SimpleUserVm } from '../models/simple-user'; + +export const simpleUserProfileFactory = (actions?: { + beforeMap: MapAction; + afterMap: MapAction; +}): MappingProfile => (mapper: Mapper) => { + const fluent = mapper.createMap(SimpleUser, SimpleUserVm); + + if (actions) { + fluent.beforeMap(actions.beforeMap).afterMap(actions.afterMap); + } + + fluent.forMember( + (d) => d.fullName, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); +}; diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/user-profile.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/user-profile.profile.ts new file mode 100644 index 000000000..64035b00e --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/user-profile.profile.ts @@ -0,0 +1,156 @@ +import { convertUsing, mapWith } from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import { dateToStringConverter } from '../converters/date-to-string.converter'; +import { Avatar, AvatarVm } from '../models/avatar'; +import { PascalAvatar, PascalAvatarVm } from '../models/avatar-pascal'; +import { SnakeAvatar, SnakeAvatarVm } from '../models/avatar-snake'; +import { UserProfile, UserProfileVm } from '../models/user-profile'; +import { + PascalUserProfile, + PascalUserProfileVm, +} from '../models/user-profile-pascal'; +import { + SnakeUserProfile, + SnakeUserProfileVm, +} from '../models/user-profile-snake'; + +export const userProfileProfile: MappingProfile = (mapper) => { + mapper + .createMap(UserProfile, UserProfileVm) + .forMember( + (d) => d.avatar, + mapWith( + () => AvatarVm, + (s) => s.avatar, + () => Avatar + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap(UserProfile, PascalUserProfileVm) + .forMember( + (d) => d.Avatar, + mapWith( + () => PascalAvatarVm, + (s) => s.avatar, + () => Avatar + ) + ) + .forMember( + (d) => d.Birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap(UserProfile, SnakeUserProfileVm) + .forMember( + (d) => d.avatar, + mapWith( + () => SnakeAvatarVm, + (s) => s.avatar, + () => Avatar + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); +}; + +export const pascalUserProfileProfile: MappingProfile = (mapper) => { + mapper + .createMap(PascalUserProfile, PascalUserProfileVm) + .forMember( + (d) => d.Avatar, + mapWith( + () => PascalAvatarVm, + (s) => s.Avatar, + () => PascalAvatar + ) + ) + .forMember( + (d) => d.Birthday, + convertUsing(dateToStringConverter, (s) => s.Birthday) + ); + + mapper + .createMap(PascalUserProfile, UserProfileVm) + .forMember( + (d) => d.avatar, + mapWith( + () => AvatarVm, + (s) => s.Avatar, + () => PascalAvatar + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.Birthday) + ); + + mapper + .createMap(PascalUserProfile, SnakeUserProfileVm) + .forMember( + (d) => d.avatar, + mapWith( + () => SnakeAvatarVm, + (s) => s.Avatar, + () => PascalAvatar + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.Birthday) + ); +}; + +export const snakeUserProfileProfile: MappingProfile = (mapper) => { + mapper + .createMap(SnakeUserProfile, SnakeUserProfileVm) + .forMember( + (d) => d.avatar, + mapWith( + () => SnakeAvatarVm, + (s) => s.avatar, + () => SnakeAvatar + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap(SnakeUserProfile, UserProfileVm) + .forMember( + (d) => d.avatar, + mapWith( + () => AvatarVm, + (s) => s.avatar, + () => SnakeAvatar + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap(SnakeUserProfile, PascalUserProfileVm) + .forMember( + (d) => d.Avatar, + mapWith( + () => PascalAvatarVm, + (s) => s.avatar, + () => SnakeAvatar + ) + ) + .forMember( + (d) => d.Birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); +}; diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/user.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/user.profile.ts new file mode 100644 index 000000000..76136ffd8 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/user.profile.ts @@ -0,0 +1,146 @@ +import { mapFrom } from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import { User, UserVm } from '../models/user'; +import { PascalUser, PascalUserVm } from '../models/user-pascal'; +import { SnakeUser, SnakeUserVm } from '../models/user-snake'; + +export const userProfile: MappingProfile = (mapper) => { + mapper + .createMap(User, UserVm) + .forMember( + (d) => d.first, + mapFrom((s) => s.firstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.lastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); + + mapper + .createMap(User, PascalUserVm) + .forMember( + (d) => d.First, + mapFrom((s) => s.firstName) + ) + .forMember( + (d) => d.Last, + mapFrom((s) => s.lastName) + ) + .forMember( + (d) => d.Full, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); + + mapper + .createMap(User, SnakeUserVm) + .forMember( + (d) => d.first, + mapFrom((s) => s.firstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.lastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); +}; + +export const pascalUserProfile: MappingProfile = (mapper) => { + mapper + .createMap(PascalUser, PascalUserVm) + .forMember( + (d) => d.First, + mapFrom((s) => s.FirstName) + ) + .forMember( + (d) => d.Last, + mapFrom((s) => s.LastName) + ) + .forMember( + (d) => d.Full, + mapFrom((s) => s.FirstName + ' ' + s.LastName) + ); + + mapper + .createMap(PascalUser, UserVm) + .forMember( + (d) => d.first, + mapFrom((s) => s.FirstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.LastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.FirstName + ' ' + s.LastName) + ); + + mapper + .createMap(PascalUser, SnakeUserVm) + .forMember( + (d) => d.first, + mapFrom((s) => s.FirstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.LastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.FirstName + ' ' + s.LastName) + ); +}; + +export const snakeUserProfile: MappingProfile = (mapper) => { + mapper + .createMap(SnakeUser, SnakeUserVm) + .forMember( + (d) => d.first, + mapFrom((s) => s.first_name) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.last_name) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.first_name + ' ' + s.last_name) + ); + + mapper + .createMap(SnakeUser, UserVm) + .forMember( + (d) => d.first, + mapFrom((s) => s.first_name) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.last_name) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.first_name + ' ' + s.last_name) + ); + + mapper + .createMap(SnakeUser, PascalUserVm) + .forMember( + (d) => d.First, + mapFrom((s) => s.first_name) + ) + .forMember( + (d) => d.Last, + mapFrom((s) => s.last_name) + ) + .forMember( + (d) => d.Full, + mapFrom((s) => s.first_name + ' ' + s.last_name) + ); +}; diff --git a/packages/integration-test/src/lib/with-classes/inheritance.spec.ts b/packages/integration-test/src/lib/with-classes/inheritance.spec.ts new file mode 100644 index 000000000..1b2362f1d --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/inheritance.spec.ts @@ -0,0 +1,57 @@ +import { setupClasses } from '../setup.spec'; +import { + EmptyFoo, + EmptyFooVm, + FooFoo, + FooFooFoo, + FooFooFooVm, + FooFooVm, +} from './fixtures/models/simple-inheritance-foo'; +import { simpleInheritanceFooProfile } from './fixtures/profiles/simple-inheritance-foo.profile'; + +describe('Inheritance', () => { + const [mapper] = setupClasses('inheritance'); + + it('should work with one level inheritance', () => { + mapper.addProfile(simpleInheritanceFooProfile); + + const foo = new FooFoo(); + foo.foo = 'foo1'; + foo.fooFoo = 'foo2'; + + const vm = mapper.map(foo, FooFooVm, FooFoo); + expect(vm).toBeTruthy(); + expect(vm.fooVm).toEqual(foo.foo); + expect(vm.fooFooVm).toEqual(foo.fooFoo); + }); + + it('should work with two level inheritance', () => { + mapper.addProfile(simpleInheritanceFooProfile); + + const foo = new FooFooFoo(); + foo.foo = 'foo1'; + foo.fooFoo = 'foo2'; + foo.fooFooFoo = 'foo3'; + + const vm = mapper.map(foo, FooFooFooVm, FooFooFoo); + expect(vm).toBeTruthy(); + expect(vm.fooVm).toEqual(foo.foo); + expect(vm.fooFooVm).toEqual(foo.fooFoo); + expect(vm.fooFooFooVm).toEqual(foo.fooFooFoo); + }); + + it('should work with empty models', () => { + mapper.addProfile(simpleInheritanceFooProfile); + + const foo = new EmptyFoo(); + foo.foo = 'foo1'; + foo.fooFoo = 'foo2'; + foo.fooFooFoo = 'foo3'; + + const vm = mapper.map(foo, EmptyFooVm, EmptyFoo); + expect(vm).toBeTruthy(); + expect(vm.fooVm).toEqual(foo.foo); + expect(vm.fooFooVm).toEqual(foo.fooFoo); + expect(vm.fooFooFooVm).toEqual(foo.fooFooFoo); + }); +}); diff --git a/packages/integration-test/src/lib/with-classes/map-action.spec.ts b/packages/integration-test/src/lib/with-classes/map-action.spec.ts new file mode 100644 index 000000000..e81794ef1 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/map-action.spec.ts @@ -0,0 +1,142 @@ +import { setupClasses } from '../setup.spec'; +import { SimpleUser, SimpleUserVm } from './fixtures/models/simple-user'; +import { simpleUserProfileFactory } from './fixtures/profiles/simple-user.profile'; + +describe('MapActions', () => { + const [mapper] = setupClasses('mapActions'); + + it('should map with mapping actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile( + simpleUserProfileFactory({ + beforeMap: beforeAction, + afterMap: afterAction, + }) + ); + + const simpleUser = new SimpleUser(); + simpleUser.firstName = 'Chau'; + simpleUser.lastName = 'Tran'; + + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + + const vm = mapper.map(simpleUser, SimpleUserVm, SimpleUser); + assertVm(simpleUser, vm); + + expect(beforeAction).toHaveBeenCalled(); + expect(afterAction).toHaveBeenCalled(); + }); + + it('should map with map actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile(simpleUserProfileFactory()); + + const simpleUser = new SimpleUser(); + simpleUser.firstName = 'Chau'; + simpleUser.lastName = 'Tran'; + + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + + const vm = mapper.map(simpleUser, SimpleUserVm, SimpleUser, { + beforeMap: beforeAction, + afterMap: afterAction, + }); + assertVm(simpleUser, vm); + + expect(beforeAction).toHaveBeenCalled(); + expect(afterAction).toHaveBeenCalled(); + }); + + it('should map actions override mapping actions', () => { + const mappingBeforeAction = jest.fn(); + const mappingAfterAction = jest.fn(); + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile( + simpleUserProfileFactory({ + beforeMap: mappingBeforeAction, + afterMap: mappingAfterAction, + }) + ); + + const simpleUser = new SimpleUser(); + simpleUser.firstName = 'Chau'; + simpleUser.lastName = 'Tran'; + + expect(mappingBeforeAction).not.toHaveBeenCalled(); + expect(mappingAfterAction).not.toHaveBeenCalled(); + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + + const vm = mapper.map(simpleUser, SimpleUserVm, SimpleUser, { + beforeMap: beforeAction, + afterMap: afterAction, + }); + assertVm(simpleUser, vm); + + // map actions should override mapping actions + expect(mappingBeforeAction).not.toHaveBeenCalled(); + expect(mappingAfterAction).not.toHaveBeenCalled(); + expect(beforeAction).toHaveBeenCalled(); + expect(afterAction).toHaveBeenCalled(); + }); + + it('should mapArray skip mapping actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile( + simpleUserProfileFactory({ + beforeMap: beforeAction, + afterMap: afterAction, + }) + ); + + const simpleUser = new SimpleUser(); + simpleUser.firstName = 'Chau'; + simpleUser.lastName = 'Tran'; + + const vms = mapper.mapArray([simpleUser], SimpleUserVm, SimpleUser); + vms.forEach((vm) => { + assertVm(simpleUser, vm); + }); + + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + }); + + it('should mapArray allow for map actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile(simpleUserProfileFactory()); + + const simpleUser = new SimpleUser(); + simpleUser.firstName = 'Chau'; + simpleUser.lastName = 'Tran'; + + const vms = mapper.mapArray([simpleUser], SimpleUserVm, SimpleUser, { + beforeMap: beforeAction, + afterMap: afterAction, + }); + vms.forEach((vm) => { + assertVm(simpleUser, vm); + }); + + expect(beforeAction).toHaveBeenCalledWith([simpleUser], []); + expect(afterAction).toHaveBeenCalledWith([simpleUser], vms); + }); + + function assertVm(user: SimpleUser, vm: SimpleUserVm) { + expect(vm.firstName).toEqual(user.firstName); + expect(vm.lastName).toEqual(user.lastName); + expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); + } +}); diff --git a/packages/integration-test/src/lib/with-classes/map.spec.ts b/packages/integration-test/src/lib/with-classes/map.spec.ts new file mode 100644 index 000000000..e47bfba00 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/map.spec.ts @@ -0,0 +1,247 @@ +import { setupClasses } from '../setup.spec'; +import { User, UserVm } from './fixtures/models/user'; +import { PascalUser, PascalUserVm } from './fixtures/models/user-pascal'; +import { + addressProfile, + pascalAddressProfile, +} from './fixtures/profiles/address.profile'; +import { + avatarProfile, + FOR_SHOULD_IGNORE_FAIL_CONDITION, + FOR_SHOULD_IGNORE_PASS_CONDITION, + pascalAvatarProfile, +} from './fixtures/profiles/avatar.profile'; +import { + pascalUserProfileProfile, + userProfileProfile, +} from './fixtures/profiles/user-profile.profile'; +import { + pascalUserProfile, + userProfile, +} from './fixtures/profiles/user.profile'; +import { getPascalUser, getUser } from './utils/get-user'; + +describe('Map - Non Flattening', () => { + const [mapper] = setupClasses('map'); + + it('should map properly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + + const vm = mapper.map(user, UserVm, User); + assertVm(user, vm); + }); + + it('should map plain object properly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + + const plain = Object.assign({}, user); + const vm = mapper.map(plain, UserVm, User); + assertVm(plain, vm); + }); + + it('should mapArray plain object properly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + + const plains = [Object.assign({}, user), Object.assign({}, user)]; + const vms = mapper.mapArray(plains, UserVm, User); + expect(vms.length).toEqual(2); + vms.forEach((vm) => { + assertVm(user, vm); + }); + }); + + it('should map correctly with condition, preCondition, and nullSubstitution', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_FAIL_CONDITION, + shouldBeSubstituted: 'will not sub', + forCondition: false, + }, + }); + + const vm = mapper.map(user, UserVm, User); + assertVm(user, vm, { shouldIgnorePassCondition: false, shouldSub: false }); + }); + + it('should mapArray correctly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + const vms = mapper.mapArray([user, user], UserVm, User); + expect(vms.length).toEqual(2); + vms.forEach((vm) => { + assertVm(user, vm); + }); + }); + + it('should mapAsync correctly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + + mapper.mapAsync(user, UserVm, User).then((vm) => { + assertVm(user, vm); + }); + }); + + it('should mapArrayAsync correctly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + mapper.mapArrayAsync([user, user], UserVm, User).then((vms) => { + expect(vms.length).toEqual(2); + vms.forEach((vm) => { + assertVm(user, vm); + }); + }); + }); + + it('should throw error when map without mapping', () => { + const user = getUser(); + expect(() => mapper.map(user, UserVm, User)).toThrow(); + }); + + it('should return empty array when mapArray with empty array', () => { + const vms = mapper.mapArray([], UserVm, User); + expect(vms).toEqual([]); + }); + + it('should map with a different casing', () => { + mapper + .addProfile(pascalAddressProfile) + .addProfile(pascalAvatarProfile) + .addProfile(pascalUserProfileProfile) + .addProfile(pascalUserProfile); + + const pascalUser = getPascalUser({ + avatar: { + ShouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + ShouldBeSubstituted: null, + ForCondition: true, + }, + }); + + const vm = mapper.map(pascalUser, PascalUserVm, PascalUser); + expect(vm).toBeTruthy(); + }); + + function assertVm( + user: User, + vm: UserVm, + assertionPaths: { + shouldIgnorePassCondition: boolean; + shouldSub: boolean; + } = { shouldIgnorePassCondition: true, shouldSub: true } + ) { + const { shouldIgnorePassCondition, shouldSub } = assertionPaths; + + expect(vm.first).toEqual(user.firstName); + expect(vm.last).toEqual(user.lastName); + expect(vm.full).toEqual(user.firstName + ' ' + user.lastName); + + expect(vm.profile.birthday).toEqual(user.profile.birthday.toDateString()); + expect(vm.profile.bio).toEqual(user.profile.bio); + + if (shouldIgnorePassCondition) { + expect(vm.profile.avatar.url).toEqual(user.profile.avatar.source); + expect(vm.profile.avatar.forCondition).toEqual( + user.profile.avatar.forCondition + ); + } else { + expect(vm.profile.avatar.url).toEqual('default url'); + expect(vm.profile.avatar.forCondition).toEqual(true); + } + + if (shouldSub) { + expect(vm.profile.avatar.shouldBeSubstituted).toEqual('sub'); + } else { + expect(vm.profile.avatar.shouldBeSubstituted).toEqual( + vm.profile.avatar.shouldBeSubstituted + ); + } + + expect(vm.profile.avatar.willBeIgnored).not.toBeTruthy(); + + user.profile.addresses.forEach((address, index) => { + expect(vm.profile.addresses[index].formattedAddress).toEqual( + `${address.street} ${address.city} ${address.state}` + ); + }); + + // no flattening + expect(vm.jobTitle).toBeUndefined(); + expect(vm.jobAnnualSalary).toBeUndefined(); + } +}); diff --git a/packages/integration-test/src/lib/with-classes/naming-conventions.spec.ts b/packages/integration-test/src/lib/with-classes/naming-conventions.spec.ts new file mode 100644 index 000000000..e048a4a8c --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/naming-conventions.spec.ts @@ -0,0 +1,308 @@ +import { + CamelCaseNamingConvention, + PascalCaseNamingConvention, + SnakeCaseNamingConvention, +} from '@automapper/core'; +import { setupClasses } from '../setup.spec'; +import { + SimpleBar, + SimpleBarVm, + SimpleFoo, + SimpleFooVm, +} from './fixtures/models/simple-foo-bar'; +import { + PascalSimpleBar, + PascalSimpleBarVm, + PascalSimpleFoo, + PascalSimpleFooVm, +} from './fixtures/models/simple-foo-bar-pascal'; +import { + SnakeSimpleBar, + SnakeSimpleBarVm, + SnakeSimpleFoo, + SnakeSimpleFooVm, +} from './fixtures/models/simple-foo-bar-snake'; +import { User, UserVm } from './fixtures/models/user'; +import { PascalUser, PascalUserVm } from './fixtures/models/user-pascal'; +import { SnakeUser, SnakeUserVm } from './fixtures/models/user-snake'; +import { + addressProfile, + pascalAddressProfile, + snakeAddressProfile, +} from './fixtures/profiles/address.profile'; +import { + avatarProfile, + pascalAvatarProfile, + snakeAvatarProfile, +} from './fixtures/profiles/avatar.profile'; +import { + pascalUserProfileProfile, + snakeUserProfileProfile, + userProfileProfile, +} from './fixtures/profiles/user-profile.profile'; +import { + pascalUserProfile, + snakeUserProfile, + userProfile, +} from './fixtures/profiles/user.profile'; +import { getPascalUser, getSnakeUser, getUser } from './utils/get-user'; + +describe('Naming Conventions', () => { + describe('with pascal <-> camel', () => { + const [mapper] = setupClasses('pascalCamel', { + source: new PascalCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + mapper.createMap(PascalSimpleBar, SimpleBarVm); + mapper.createMap(PascalSimpleFoo, SimpleFooVm); + + const foo = new PascalSimpleFoo(); + foo.Foo = 'Foo'; + foo.FooBar = 123; + foo.Bar = new PascalSimpleBar(); + foo.Bar.Bar = 'Bar'; + + const vm = mapper.map(foo, SimpleFooVm, PascalSimpleFoo); + expect(vm.foo).toEqual(foo.Foo); + expect(vm.bar.bar).toEqual(foo.Bar.Bar); + expect(vm.fooBar).toEqual(foo.FooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(pascalAddressProfile) + .addProfile(pascalAvatarProfile) + .addProfile(pascalUserProfileProfile) + .addProfile(pascalUserProfile); + + const user = getPascalUser(); + + const vm = mapper.map(user, UserVm, PascalUser); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.jobTitle).toEqual(user.Job.Title); + expect(vm.jobAnnualSalary).toEqual(user.Job.AnnualSalary); + }); + }); + + describe('with camel <-> pascal', () => { + const [mapper] = setupClasses('camelPascal', { + source: new CamelCaseNamingConvention(), + destination: new PascalCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + mapper.createMap(SimpleBar, PascalSimpleBarVm); + mapper.createMap(SimpleFoo, PascalSimpleFooVm); + + const foo = new SimpleFoo(); + foo.foo = 'Foo'; + foo.fooBar = 123; + foo.bar = new SimpleBar(); + foo.bar.bar = 'Bar'; + + const vm = mapper.map(foo, PascalSimpleFooVm, SimpleFoo); + expect(vm.Foo).toEqual(foo.foo); + expect(vm.Bar.Bar).toEqual(foo.bar.bar); + expect(vm.FooBar).toEqual(foo.fooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser(); + + const vm = mapper.map(user, PascalUserVm, User); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.JobTitle).toEqual(user.job.title); + expect(vm.JobAnnualSalary).toEqual(user.job.annualSalary); + }); + }); + + describe('with snake <-> camel', () => { + const [mapper] = setupClasses('snakeCamel', { + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + mapper.createMap(SnakeSimpleBar, SimpleBarVm); + mapper.createMap(SnakeSimpleFoo, SimpleFooVm); + + const foo = new SnakeSimpleFoo(); + foo.foo = 'Foo'; + foo.foo_bar = 123; + foo.bar = new SimpleBar(); + foo.bar.bar = 'Bar'; + + const vm = mapper.map(foo, SimpleFooVm, SnakeSimpleFoo); + expect(vm.foo).toEqual(foo.foo); + expect(vm.bar.bar).toEqual(foo.bar.bar); + expect(vm.fooBar).toEqual(foo.foo_bar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(snakeAddressProfile) + .addProfile(snakeAvatarProfile) + .addProfile(snakeUserProfileProfile) + .addProfile(snakeUserProfile); + + const user = getSnakeUser(); + + const vm = mapper.map(user, UserVm, SnakeUser); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.jobTitle).toEqual(user.job.title); + expect(vm.jobAnnualSalary).toEqual(user.job.annual_salary); + }); + }); + + describe('with camel <-> snake', () => { + const [mapper] = setupClasses('camelSnake', { + source: new CamelCaseNamingConvention(), + destination: new SnakeCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + mapper.createMap(SimpleBar, SnakeSimpleBarVm); + mapper.createMap(SimpleFoo, SnakeSimpleFooVm); + + const foo = new SimpleFoo(); + foo.foo = 'Foo'; + foo.fooBar = 123; + foo.bar = new SimpleBar(); + foo.bar.bar = 'Bar'; + + const vm = mapper.map(foo, SnakeSimpleFooVm, SimpleFoo); + expect(vm.foo).toEqual(foo.foo); + expect(vm.bar.bar).toEqual(foo.bar.bar); + expect(vm.foo_bar).toEqual(foo.fooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser(); + + const vm = mapper.map(user, SnakeUserVm, User); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.job_title).toEqual(user.job.title); + expect(vm.job_annual_salary).toEqual(user.job.annualSalary); + }); + }); + + describe('with pascal <-> snake', () => { + const [mapper] = setupClasses('pascalSnake', { + source: new PascalCaseNamingConvention(), + destination: new SnakeCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + mapper.createMap(PascalSimpleBar, SnakeSimpleBarVm); + mapper.createMap(PascalSimpleFoo, SnakeSimpleFooVm); + + const foo = new PascalSimpleFoo(); + foo.Foo = 'Foo'; + foo.FooBar = 123; + foo.Bar = new PascalSimpleBar(); + foo.Bar.Bar = 'Bar'; + + const vm = mapper.map(foo, SnakeSimpleFooVm, PascalSimpleFoo); + expect(vm.foo).toEqual(foo.Foo); + expect(vm.bar.bar).toEqual(foo.Bar.Bar); + expect(vm.foo_bar).toEqual(foo.FooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(pascalAddressProfile) + .addProfile(pascalAvatarProfile) + .addProfile(pascalUserProfileProfile) + .addProfile(pascalUserProfile); + + const user = getPascalUser(); + + const vm = mapper.map(user, SnakeUserVm, PascalUser); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.job_title).toEqual(user.Job.Title); + expect(vm.job_annual_salary).toEqual(user.Job.AnnualSalary); + }); + }); + + describe('with snake <-> pascal', () => { + const [mapper] = setupClasses('snakePascal', { + source: new SnakeCaseNamingConvention(), + destination: new PascalCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + mapper.createMap(SnakeSimpleBar, PascalSimpleBarVm); + mapper.createMap(SnakeSimpleFoo, PascalSimpleFooVm); + + const foo = new SnakeSimpleFoo(); + foo.foo = 'Foo'; + foo.foo_bar = 123; + foo.bar = new SnakeSimpleBar(); + foo.bar.bar = 'Bar'; + + const vm = mapper.map(foo, PascalSimpleFooVm, SnakeSimpleFoo); + expect(vm.Foo).toEqual(foo.foo); + expect(vm.Bar.Bar).toEqual(foo.bar.bar); + expect(vm.FooBar).toEqual(foo.foo_bar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(snakeAddressProfile) + .addProfile(snakeAvatarProfile) + .addProfile(snakeUserProfileProfile) + .addProfile(snakeUserProfile); + + const user = getSnakeUser(); + + const vm = mapper.map(user, PascalUserVm, SnakeUser); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.JobTitle).toEqual(user.job.title); + expect(vm.JobAnnualSalary).toEqual(user.job.annual_salary); + }); + }); +}); diff --git a/packages/integration-test/src/lib/with-classes/utils/get-user.ts b/packages/integration-test/src/lib/with-classes/utils/get-user.ts new file mode 100644 index 000000000..251dabd1c --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/utils/get-user.ts @@ -0,0 +1,138 @@ +import { Address } from '../fixtures/models/address'; +import { PascalAddress } from '../fixtures/models/address-pascal'; +import { SnakeAddress } from '../fixtures/models/address-snake'; +import { Avatar } from '../fixtures/models/avatar'; +import { PascalAvatar } from '../fixtures/models/avatar-pascal'; +import { SnakeAvatar } from '../fixtures/models/avatar-snake'; +import { Job } from '../fixtures/models/job'; +import { PascalJob } from '../fixtures/models/job-pascal'; +import { SnakeJob } from '../fixtures/models/job-snake'; +import { User } from '../fixtures/models/user'; +import { PascalUser } from '../fixtures/models/user-pascal'; +import { UserProfile } from '../fixtures/models/user-profile'; +import { PascalUserProfile } from '../fixtures/models/user-profile-pascal'; +import { SnakeUserProfile } from '../fixtures/models/user-profile-snake'; +import { SnakeUser } from '../fixtures/models/user-snake'; + +export function getUser( + partials: { + user?: Partial; + job?: Partial; + profile?: Partial; + avatar?: Partial; + } = {} +) { + const userProfile = new UserProfile(); + userProfile.bio = 'Introvert-ish'; + userProfile.birthday = new Date('10/14/1991'); + + const address1 = new Address(); + address1.street = '123 Acme Dr'; + address1.city = 'Sim'; + address1.state = 'Show Me'; + + const address2 = new Address(); + address2.street = '456 Rubik Dr'; + address2.city = 'Some'; + address2.state = 'October'; + userProfile.addresses = [address1, address2]; + + const avatar = new Avatar(); + avatar.source = 'Internet'; + avatar.url = 'url.com'; + userProfile.avatar = Object.assign(avatar, partials.avatar ?? {}); + + const user = new User(); + user.firstName = 'Chau'; + user.lastName = 'Tran'; + + const userJob = new Job(); + userJob.title = 'Developer'; + userJob.annualSalary = 99999; + user.job = Object.assign(userJob, partials.job ?? {}); + + user.profile = Object.assign(userProfile, partials.profile ?? {}); + return Object.assign(user, partials.user ?? {}); +} + +export function getPascalUser( + partials: { + user?: Partial; + job?: Partial; + profile?: Partial; + avatar?: Partial; + } = {} +) { + const userProfile = new PascalUserProfile(); + userProfile.Bio = 'Introvert-ish'; + userProfile.Birthday = new Date('10/14/1991'); + + const address1 = new PascalAddress(); + address1.Street = '123 Acme Dr'; + address1.City = 'Sim'; + address1.State = 'Show Me'; + + const address2 = new PascalAddress(); + address2.Street = '456 Rubik Dr'; + address2.City = 'Some'; + address2.State = 'October'; + userProfile.Addresses = [address1, address2]; + + const avatar = new PascalAvatar(); + avatar.Source = 'Internet'; + avatar.Url = 'url.com'; + userProfile.Avatar = Object.assign(avatar, partials.avatar ?? {}); + + const user = new PascalUser(); + user.FirstName = 'Chau'; + user.LastName = 'Tran'; + + const userJob = new PascalJob(); + userJob.Title = 'Developer'; + userJob.AnnualSalary = 99999; + user.Job = Object.assign(userJob, partials.job ?? {}); + + user.Profile = Object.assign(userProfile, partials.profile ?? {}); + return Object.assign(user, partials.user ?? {}); +} + +export function getSnakeUser( + partials: { + user?: Partial; + job?: Partial; + profile?: Partial; + avatar?: Partial; + } = {} +) { + const userProfile = new SnakeUserProfile(); + userProfile.bio = 'Introvert-ish'; + userProfile.birthday = new Date('10/14/1991'); + + const address1 = new SnakeAddress(); + address1.street = '123 Acme Dr'; + address1.city = 'Sim'; + address1.state = 'Show Me'; + + const address2 = new SnakeAddress(); + address2.street = '456 Rubik Dr'; + address2.city = 'Some'; + address2.state = 'October'; + userProfile.addresses = [address1, address2]; + + const avatar = new SnakeAvatar(); + avatar.source = 'Internet'; + avatar.url = 'url.com'; + userProfile.avatar = Object.assign(avatar, partials.avatar ?? {}); + + const user = new SnakeUser(); + user.first_name = 'Chau'; + user.last_name = 'Tran'; + + const userJob = new SnakeJob(); + userJob.title = 'Developer'; + userJob.annual_salary = 99999; + user.job = Object.assign(userJob, partials.job ?? {}); + + user.profile = Object.assign(userProfile, partials.profile ?? {}); + return Object.assign(user, partials.user ?? {}); +} diff --git a/packages/integration-test/src/lib/with-classes/variants.spec.ts b/packages/integration-test/src/lib/with-classes/variants.spec.ts new file mode 100644 index 000000000..e7f0e44a0 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/variants.spec.ts @@ -0,0 +1,68 @@ +import { mapFrom } from '@automapper/core'; +import { setupClasses } from '../setup.spec'; +import { SimpleUserVm } from './fixtures/models/simple-user'; +import { + UserWithGetter, + UserWithReturnKeyword, + UserWithReturnKeywordVm, +} from './fixtures/models/user-variants'; + +describe('Variants', () => { + const [mapper] = setupClasses('variants'); + + it('should map with model with getters', () => { + mapper.createMap(UserWithGetter, SimpleUserVm).forMember( + (d) => d.fullName, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); + + const user = new UserWithGetter(); + user.firstName = 'Chau'; + user.lastName = 'Tran'; + + const vm = mapper.map(user, SimpleUserVm, UserWithGetter); + expect(vm.firstName).toEqual(user.firstName); + expect(vm.lastName).toEqual(user.lastName); + expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); + }); + + it('should map with model with properties with return keyword', () => { + mapper + .createMap(UserWithReturnKeyword, UserWithReturnKeywordVm) + .forMember( + (d) => { + return d.returnReturnFirst; + }, + mapFrom((s) => { + return s.returnFirstName; + }) + ) + .forMember( + function (d) { + return d.returnReturnLast; + }, + mapFrom(function (s) { + return s.returnLastName; + }) + ) + .forMember( + (d) => { + return d.returnReturnFull; + }, + mapFrom(function (s) { + return s.returnFirstName + ' ' + s.returnLastName; + }) + ); + + const user = new UserWithReturnKeyword(); + user.returnFirstName = 'Chau'; + user.returnLastName = 'Tran'; + + const vm = mapper.map(user, UserWithReturnKeywordVm, UserWithReturnKeyword); + expect(vm.returnReturnFirst).toEqual(user.returnFirstName); + expect(vm.returnReturnLast).toEqual(user.returnLastName); + expect(vm.returnReturnFull).toEqual( + user.returnFirstName + ' ' + user.returnLastName + ); + }); +}); diff --git a/packages/integration-test/src/lib/with-pojos/creation.spec.ts b/packages/integration-test/src/lib/with-pojos/creation.spec.ts new file mode 100644 index 000000000..10bb4b959 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/creation.spec.ts @@ -0,0 +1,58 @@ +import { MappingProfile } from '@automapper/types'; +import { setupPojos } from '../setup.spec'; +import type { + SimpleBar, + SimpleBarVm, + SimpleFoo, + SimpleFooVm, +} from './fixtures/interfaces/simple-foo-bar.interface'; +import { createSimpleFooBarMetadata } from './fixtures/interfaces/simple-foo-bar.interface'; + +describe('Creation', () => { + const [mapper, spiedErrorHandler] = setupPojos('creation'); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should create mapping', () => { + createSimpleFooBarMetadata(); + + expect(mapper.getMapping('SimpleBar', 'SimpleBarVm')).toBeUndefined(); + expect(mapper.getMapping('SimpleFoo', 'SimpleFooVm')).toBeUndefined(); + + expect(spiedErrorHandler).toHaveBeenCalledTimes(2); + + mapper.createMap('SimpleBar', 'SimpleBarVm'); + mapper.createMap('SimpleFoo', 'SimpleFooVm'); + + expect(mapper.getMapping('SimpleBar', 'SimpleBarVm')).toBeTruthy(); + expect(mapper.getMapping('SimpleFoo', 'SimpleFooVm')).toBeTruthy(); + }); + + it('should throw when creating duplicate mapping', () => { + createSimpleFooBarMetadata(); + + mapper.createMap('SimpleBar', 'SimpleBarVm'); + expect(mapper.getMapping('SimpleBar', 'SimpleBarVm')).toBeTruthy(); + + mapper.createMap('SimpleBar', 'SimpleBarVm'); + expect(spiedErrorHandler).toHaveBeenNthCalledWith( + 1, + `Mapping for source SimpleBar and destination SimpleBarVm already exists` + ); + }); + + it('should add profile', () => { + const profile: MappingProfile = (_mapper) => { + createSimpleFooBarMetadata(); + _mapper.createMap('SimpleBar', 'SimpleBarVm'); + _mapper.createMap('SimpleFoo', 'SimpleFooVm'); + }; + + mapper.addProfile(profile); + + expect(mapper.getMapping('SimpleBar', 'SimpleBarVm')).toBeTruthy(); + expect(mapper.getMapping('SimpleFoo', 'SimpleFooVm')).toBeTruthy(); + }); +}); diff --git a/packages/integration-test/src/lib/with-pojos/defer.spec.ts b/packages/integration-test/src/lib/with-pojos/defer.spec.ts new file mode 100644 index 000000000..dbe62bbac --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/defer.spec.ts @@ -0,0 +1,64 @@ +import { mapDefer, mapFrom, preCondition } from '@automapper/core'; +import { setupPojos } from '../setup.spec'; +import { + createSimpleUserMetadata, + SimpleUser, + SimpleUserVm, +} from './fixtures/interfaces/simple-user.interface'; + +describe('Defer', () => { + const [mapper] = setupPojos('defer'); + + it('should map correctly', () => { + createSimpleUserMetadata(); + mapper + .createMap('SimpleUser', 'SimpleUserVm') + .forMember( + (d) => d.fullName, + mapDefer(() => mapFrom((s) => s.firstName + ' ' + s.lastName)) + ); + + const user = { + firstName: 'Chau', + lastName: 'Tran', + } as SimpleUser; + + const vm = mapper.map( + user, + 'SimpleUserVm', + 'SimpleUser' + ); + expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); + }); + + it('should map correctly with preCond', () => { + createSimpleUserMetadata(); + mapper + .createMap('SimpleUser', 'SimpleUserVm') + .forMember( + (d) => d.fullName, + preCondition((s) => s.firstName === 'Phuong'), + mapDefer(() => mapFrom((s) => s.firstName + ' ' + s.lastName)) + ); + + const user = { + firstName: 'Chau', + lastName: 'Tran', + } as SimpleUser; + + let vm = mapper.map( + user, + 'SimpleUserVm', + 'SimpleUser' + ); + expect(vm.fullName).toEqual(undefined); + + user.firstName = 'Phuong'; + vm = mapper.map( + user, + 'SimpleUserVm', + 'SimpleUser' + ); + expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); + }); +}); diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/converters/date-to-string.converter.ts b/packages/integration-test/src/lib/with-pojos/fixtures/converters/date-to-string.converter.ts new file mode 100644 index 000000000..7dbacb430 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/converters/date-to-string.converter.ts @@ -0,0 +1,7 @@ +import type { Converter } from '@automapper/types'; + +export const dateToStringConverter: Converter = { + convert(source: Date): string { + return source.toDateString(); + }, +}; diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address-pascal.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address-pascal.interface.ts new file mode 100644 index 000000000..22c2e0bd0 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address-pascal.interface.ts @@ -0,0 +1,22 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface PascalAddress { + Street: string; + City: string; + State: string; +} + +export interface PascalAddressVm { + FormattedAddress: string; +} + +export function createAddressPascalMetadata() { + createMetadataMap('PascalAddress', { + Street: String, + City: String, + State: String, + }); + createMetadataMap('PascalAddressVm', { + FormattedAddress: String, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address-snake.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address-snake.interface.ts new file mode 100644 index 000000000..42feba683 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address-snake.interface.ts @@ -0,0 +1,22 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface SnakeAddress { + street: string; + city: string; + state: string; +} + +export interface SnakeAddressVm { + formatted_address: string; +} + +export function createAddressSnakeMetadata() { + createMetadataMap('SnakeAddress', { + street: String, + city: String, + state: String, + }); + createMetadataMap('SnakeAddressVm', { + formatted_address: String, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address.interface.ts new file mode 100644 index 000000000..e0ef4cb0f --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/address.interface.ts @@ -0,0 +1,22 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface Address { + street: string; + city: string; + state: string; +} + +export interface AddressVm { + formattedAddress: string; +} + +export function createAddressMetadata() { + createMetadataMap('Address', { + street: String, + city: String, + state: String, + }); + createMetadataMap('AddressVm', { + formattedAddress: String, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar-pascal.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar-pascal.interface.ts new file mode 100644 index 000000000..f030b0e19 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar-pascal.interface.ts @@ -0,0 +1,32 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface PascalAvatar { + Url: string; + Source: string; + ShouldIgnore: number; + ShouldBeSubstituted: string; + ForCondition: boolean; +} + +export interface PascalAvatarVm { + Url: string; + WillBeIgnored: number; + ShouldBeSubstituted: string; + ForCondition: boolean; +} + +export function createAvatarPascalMetadata() { + createMetadataMap('PascalAvatar', { + Url: String, + Source: String, + ShouldIgnore: Number, + ShouldBeSubstituted: String, + ForCondition: Boolean, + }); + + createMetadataMap('PascalAvatarVm', 'PascalAvatar', { + Source: null, + ShouldIgnore: null, + WillBeIgnored: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar-snake.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar-snake.interface.ts new file mode 100644 index 000000000..581cf063e --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar-snake.interface.ts @@ -0,0 +1,32 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface SnakeAvatar { + url: string; + source: string; + should_ignore: number; + should_be_substituted: string; + for_condition: boolean; +} + +export interface SnakeAvatarVm { + url: string; + will_be_ignored: number; + should_be_substituted: string; + for_condition: boolean; +} + +export function createAvatarSnakeMetadata() { + createMetadataMap('SnakeAvatar', { + url: String, + source: String, + should_ignore: Number, + should_be_substituted: String, + for_condition: Boolean, + }); + + createMetadataMap('SnakeAvatarVm', 'SnakeAvatar', { + source: null, + should_ignore: null, + will_be_ignored: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar.interface.ts new file mode 100644 index 000000000..eb87a39ef --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/avatar.interface.ts @@ -0,0 +1,32 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface Avatar { + url: string; + source: string; + shouldIgnore: number; + shouldBeSubstituted: string; + forCondition: boolean; +} + +export interface AvatarVm { + url: string; + willBeIgnored: number; + shouldBeSubstituted: string; + forCondition: boolean; +} + +export function createAvatarMetadata() { + createMetadataMap('Avatar', { + url: String, + source: String, + shouldIgnore: Number, + shouldBeSubstituted: String, + forCondition: Boolean, + }); + + createMetadataMap('AvatarVm', 'Avatar', { + source: null, + shouldIgnore: null, + willBeIgnored: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/circular-deps-bar.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/circular-deps-bar.interface.ts new file mode 100644 index 000000000..e0b281717 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/circular-deps-bar.interface.ts @@ -0,0 +1,16 @@ +import { AutoMap } from '@automapper/classes'; +import { FooWithBar, FooWithBarZeroDepth } from './circular-deps-foo.interface'; + +export class BarWithFoo { + @AutoMap() + id: string; + @AutoMap(() => FooWithBar, 1) + foo: FooWithBar; +} + +export class BarWithFooZeroDepth { + @AutoMap() + id: string; + @AutoMap(() => FooWithBarZeroDepth) + foo: FooWithBarZeroDepth; +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/circular-deps-foo.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/circular-deps-foo.interface.ts new file mode 100644 index 000000000..4908d18a5 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/circular-deps-foo.interface.ts @@ -0,0 +1,16 @@ +import { AutoMap } from '@automapper/classes'; +import { BarWithFoo, BarWithFooZeroDepth } from './circular-deps-bar.interface'; + +export class FooWithBar { + @AutoMap() + id: string; + @AutoMap(() => BarWithFoo, 1) + bar: BarWithFoo | null; +} + +export class FooWithBarZeroDepth { + @AutoMap() + id: string; + @AutoMap(() => BarWithFooZeroDepth) + bar: BarWithFooZeroDepth | null; +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job-pascal.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job-pascal.interface.ts new file mode 100644 index 000000000..abc858e59 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job-pascal.interface.ts @@ -0,0 +1,13 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface PascalJob { + Title: string; + AnnualSalary: number; +} + +export function createJobPascalMetadata() { + createMetadataMap('PascalJob', { + Title: String, + AnnualSalary: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job-snake.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job-snake.interface.ts new file mode 100644 index 000000000..55978bb35 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job-snake.interface.ts @@ -0,0 +1,13 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface SnakeJob { + title: string; + annual_salary: number; +} + +export function createJobSnakeMetadata() { + createMetadataMap('SnakeJob', { + title: String, + annual_salary: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job.interface.ts new file mode 100644 index 000000000..bea3711b5 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/job.interface.ts @@ -0,0 +1,13 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface Job { + title: string; + annualSalary: number; +} + +export function createJobMetadata() { + createMetadataMap('Job', { + title: String, + annualSalary: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar-pascal.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar-pascal.interface.ts new file mode 100644 index 000000000..8a435e89e --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar-pascal.interface.ts @@ -0,0 +1,34 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface PascalSimpleBar { + Bar: string; +} + +export interface PascalSimpleBarVm { + Bar: string; +} + +export interface PascalSimpleFoo { + Foo: string; + Bar: PascalSimpleBar; + FooBar: number; +} + +export interface PascalSimpleFooVm { + Foo: string; + Bar: PascalSimpleBarVm; + FooBar: number; +} + +export function createSimpleFooBarPascalMetadata() { + createMetadataMap('PascalSimpleBar', { Bar: String }); + createMetadataMap('PascalSimpleBarVm', 'PascalSimpleBar'); + createMetadataMap('PascalSimpleFoo', { + Foo: String, + Bar: 'PascalSimpleBar', + FooBar: Number, + }); + createMetadataMap('PascalSimpleFooVm', 'PascalSimpleFoo', { + Bar: 'PascalSimpleBarVm', + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar-snake.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar-snake.interface.ts new file mode 100644 index 000000000..ee2db4c44 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar-snake.interface.ts @@ -0,0 +1,34 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface SnakeSimpleBar { + bar: string; +} + +export interface SnakeSimpleBarVm { + bar: string; +} + +export interface SnakeSimpleFoo { + foo: string; + bar: SnakeSimpleBar; + foo_bar: number; +} + +export interface SnakeSimpleFooVm { + foo: string; + bar: SnakeSimpleBarVm; + foo_bar: number; +} + +export function createSimpleFooBarSnakeMetadata() { + createMetadataMap('SnakeSimpleBar', { bar: String }); + createMetadataMap('SnakeSimpleBarVm', 'SnakeSimpleBar'); + createMetadataMap('SnakeSimpleFoo', { + foo: String, + bar: 'SnakeSimpleBar', + foo_bar: Number, + }); + createMetadataMap('SnakeSimpleFooVm', 'SnakeSimpleFoo', { + bar: 'SnakeSimpleBarVm', + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar.interface.ts new file mode 100644 index 000000000..0c207f2f5 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-foo-bar.interface.ts @@ -0,0 +1,34 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface SimpleBar { + bar: string; +} + +export interface SimpleBarVm { + bar: string; +} + +export interface SimpleFoo { + foo: string; + bar: SimpleBar; + fooBar: number; +} + +export interface SimpleFooVm { + foo: string; + bar: SimpleBarVm; + fooBar: number; +} + +export function createSimpleFooBarMetadata() { + createMetadataMap('SimpleBar', { bar: String }); + createMetadataMap('SimpleBarVm', 'SimpleBar'); + createMetadataMap('SimpleFoo', { + foo: String, + bar: 'SimpleBar', + fooBar: Number, + }); + createMetadataMap('SimpleFooVm', 'SimpleFoo', { + bar: 'SimpleBarVm', + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-inheritance-foo.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-inheritance-foo.interface.ts new file mode 100644 index 000000000..635eba13b --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-inheritance-foo.interface.ts @@ -0,0 +1,46 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface Foo { + foo: string; +} + +export interface FooVm { + fooVm: string; +} + +export interface FooFoo extends Foo { + fooFoo: string; +} + +export interface FooFooVm extends FooVm { + fooFooVm: string; +} + +export interface FooFooFoo extends FooFoo { + fooFooFoo: string; +} + +export interface FooFooFooVm extends FooFooVm { + fooFooFooVm: string; +} + +export function createSimpleInheritanceFooMetadata() { + createMetadataMap('Foo', { + foo: String, + }); + createMetadataMap('FooVm', { + fooVm: String, + }); + createMetadataMap('FooFoo', 'Foo', { + fooFoo: String, + }); + createMetadataMap('FooFooVm', 'FooVm', { + fooFooVm: String, + }); + createMetadataMap('FooFooFoo', 'FooFoo', { + fooFooFoo: String, + }); + createMetadataMap('FooFooFooVm', 'FooFooVm', { + fooFooFooVm: String, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-user.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-user.interface.ts new file mode 100644 index 000000000..548876333 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/simple-user.interface.ts @@ -0,0 +1,22 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface SimpleUser { + firstName: string; + lastName: string; +} + +export interface SimpleUserVm { + firstName: string; + lastName: string; + fullName: string; +} + +export function createSimpleUserMetadata() { + createMetadataMap('SimpleUser', { + firstName: String, + lastName: String, + }); + createMetadataMap('SimpleUserVm', 'SimpleUser', { + fullName: String, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-pascal.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-pascal.interface.ts new file mode 100644 index 000000000..95a5649c9 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-pascal.interface.ts @@ -0,0 +1,39 @@ +import { createMetadataMap } from '@automapper/pojos'; +import { PascalJob } from './job-pascal.interface'; +import { + PascalUserProfile, + PascalUserProfileVm, +} from './user-profile-pascal.interface'; + +export interface PascalUser { + FirstName: string; + LastName: string; + Profile: PascalUserProfile; + Job: PascalJob; +} + +export interface PascalUserVm { + First: string; + Last: string; + Full: string; + Profile: PascalUserProfileVm; + JobTitle: string; + JobAnnualSalary: number; +} + +export function createUserPascalMetadata() { + createMetadataMap('PascalUser', { + FirstName: String, + LastName: String, + Profile: 'PascalUserProfile', + Job: 'Job', + }); + createMetadataMap('PascalUserVm', { + First: String, + Last: String, + Full: String, + Profile: 'PascalUserProfileVm', + JobTitle: String, + JobAnnualSalary: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile-pascal.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile-pascal.interface.ts new file mode 100644 index 000000000..508fc2460 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile-pascal.interface.ts @@ -0,0 +1,30 @@ +import { createMetadataMap } from '@automapper/pojos'; +import { PascalAddress, PascalAddressVm } from './address-pascal.interface'; +import { PascalAvatar, PascalAvatarVm } from './avatar-pascal.interface'; + +export interface PascalUserProfile { + Bio: string; + Birthday: Date; + Avatar: PascalAvatar; + Addresses: PascalAddress[]; +} + +export interface PascalUserProfileVm { + Bio: string; + Birthday: string; + Avatar: PascalAvatarVm; + Addresses: PascalAddressVm[]; +} + +export function createUserProfilePascalMetadata() { + createMetadataMap('PascalUserProfile', { + Bio: String, + Birthday: String, + Avatar: 'PascalAvatar', + Addresses: 'PascalAddress', + }); + createMetadataMap('PascalUserProfileVm', 'PascalUserProfile', { + Avatar: 'PascalAvatarVm', + Addresses: 'PascalAddressVm', + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile-snake.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile-snake.interface.ts new file mode 100644 index 000000000..541b9e6e5 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile-snake.interface.ts @@ -0,0 +1,30 @@ +import { createMetadataMap } from '@automapper/pojos'; +import { SnakeAddress, SnakeAddressVm } from './address-snake.interface'; +import { SnakeAvatar, SnakeAvatarVm } from './avatar-snake.interface'; + +export interface SnakeUserProfile { + bio: string; + birthday: Date; + avatar: SnakeAvatar; + addresses: SnakeAddress[]; +} + +export interface SnakeUserProfileVm { + bio: string; + birthday: string; + avatar: SnakeAvatarVm; + addresses: SnakeAddressVm[]; +} + +export function createUserProfileSnakeMetadata() { + createMetadataMap('SnakeUserProfile', { + bio: String, + birthday: String, + avatar: 'SnakeAvatar', + addresses: 'SnakeAddress', + }); + createMetadataMap('SnakeUserProfileVm', 'SnakeUserProfile', { + avatar: 'SnakeAvatarVm', + addresses: 'SnakeAddressVm', + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile.interface.ts new file mode 100644 index 000000000..70881a70a --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-profile.interface.ts @@ -0,0 +1,30 @@ +import { createMetadataMap } from '@automapper/pojos'; +import { Address, AddressVm } from './address.interface'; +import { Avatar, AvatarVm } from './avatar.interface'; + +export interface UserProfile { + bio: string; + birthday: Date; + avatar: Avatar; + addresses: Address[]; +} + +export interface UserProfileVm { + bio: string; + birthday: string; + avatar: AvatarVm; + addresses: AddressVm[]; +} + +export function createUserProfileMetadata() { + createMetadataMap('UserProfile', { + bio: String, + birthday: String, + avatar: 'Avatar', + addresses: 'Address', + }); + createMetadataMap('UserProfileVm', 'UserProfile', { + avatar: 'AvatarVm', + addresses: 'AddressVm', + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-snake.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-snake.interface.ts new file mode 100644 index 000000000..f7285181c --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-snake.interface.ts @@ -0,0 +1,39 @@ +import { createMetadataMap } from '@automapper/pojos'; +import { SnakeJob } from './job-snake.interface'; +import { + SnakeUserProfile, + SnakeUserProfileVm, +} from './user-profile-snake.interface'; + +export interface SnakeUser { + first_name: string; + last_name: string; + profile: SnakeUserProfile; + job: SnakeJob; +} + +export interface SnakeUserVm { + first: string; + last: string; + full: string; + profile: SnakeUserProfileVm; + job_title: string; + job_annual_salary: number; +} + +export function createUserSnakeMetadata() { + createMetadataMap('SnakeUser', { + first_name: String, + last_name: String, + profile: 'SnakeUserProfile', + job: 'SnakeJob', + }); + createMetadataMap('SnakeUserVm', { + first: String, + last: String, + full: String, + profile: 'SnakeUserProfileVm', + job_title: String, + job_annual_salary: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-variants.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-variants.interface.ts new file mode 100644 index 000000000..ae671c98b --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user-variants.interface.ts @@ -0,0 +1,25 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface UserWithReturnKeyword { + returnFirstName: string; + returnLastName: string; +} + +export interface UserWithReturnKeywordVm { + returnReturnFirst: string; + returnReturnLast: string; + returnReturnFull: string; +} + +export function createUserVariantsMetadata() { + createMetadataMap('UserWithReturnKeyword', { + returnFirstName: String, + returnLastName: String, + }); + + createMetadataMap('UserWithReturnKeywordVm', { + returnReturnFirst: String, + returnReturnLast: String, + returnReturnFull: String, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user.interface.ts new file mode 100644 index 000000000..dd8743daf --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/user.interface.ts @@ -0,0 +1,36 @@ +import { createMetadataMap } from '@automapper/pojos'; +import { Job } from './job.interface'; +import { UserProfile, UserProfileVm } from './user-profile.interface'; + +export interface User { + firstName: string; + lastName: string; + profile: UserProfile; + job: Job; +} + +export interface UserVm { + first: string; + last: string; + full: string; + profile: UserProfileVm; + jobTitle: string; + jobAnnualSalary: number; +} + +export function createUserMetadata() { + createMetadataMap('User', { + firstName: String, + lastName: String, + profile: 'UserProfile', + job: 'Job', + }); + createMetadataMap('UserVm', { + first: String, + last: String, + full: String, + profile: 'UserProfileVm', + jobTitle: String, + jobAnnualSalary: Number, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/profiles/address.profile.ts b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/address.profile.ts new file mode 100644 index 000000000..be95d3d0b --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/address.profile.ts @@ -0,0 +1,90 @@ +import { mapFrom } from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import type { + PascalAddress, + PascalAddressVm, +} from '../interfaces/address-pascal.interface'; +import { createAddressPascalMetadata } from '../interfaces/address-pascal.interface'; +import type { + SnakeAddress, + SnakeAddressVm, +} from '../interfaces/address-snake.interface'; +import { createAddressSnakeMetadata } from '../interfaces/address-snake.interface'; +import type { Address, AddressVm } from '../interfaces/address.interface'; +import { createAddressMetadata } from '../interfaces/address.interface'; + +export const addressProfile: MappingProfile = (mapper) => { + createAddressMetadata(); + createAddressPascalMetadata(); + createAddressSnakeMetadata(); + + mapper.createMap('Address', 'AddressVm').forMember( + (d) => d.formattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper + .createMap('Address', 'PascalAddressVm') + .forMember( + (d) => d.FormattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper + .createMap('Address', 'SnakeAddressVm') + .forMember( + (d) => d.formatted_address, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); +}; + +export const pascalAddressProfile: MappingProfile = (mapper) => { + createAddressMetadata(); + createAddressPascalMetadata(); + createAddressSnakeMetadata(); + + mapper + .createMap( + 'PascalAddress', + 'PascalAddressVm' + ) + .forMember( + (d) => d.FormattedAddress, + mapFrom((s) => `${s.Street} ${s.City} ${s.State}`) + ); + mapper + .createMap('PascalAddress', 'AddressVm') + .forMember( + (d) => d.formattedAddress, + mapFrom((s) => `${s.Street} ${s.City} ${s.State}`) + ); + mapper + .createMap('PascalAddress', 'SnakeAddressVm') + .forMember( + (d) => d.formatted_address, + mapFrom((s) => `${s.Street} ${s.City} ${s.State}`) + ); +}; + +export const snakeAddressProfile: MappingProfile = (mapper) => { + createAddressMetadata(); + createAddressPascalMetadata(); + createAddressSnakeMetadata(); + + mapper + .createMap('SnakeAddress', 'SnakeAddressVm') + .forMember( + (d) => d.formatted_address, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper + .createMap('SnakeAddress', 'AddressVm') + .forMember( + (d) => d.formattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); + mapper + .createMap('SnakeAddress', 'PascalAddressVm') + .forMember( + (d) => d.FormattedAddress, + mapFrom((s) => `${s.street} ${s.city} ${s.state}`) + ); +}; diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/profiles/avatar.profile.ts b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/avatar.profile.ts new file mode 100644 index 000000000..cc1fc8e82 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/avatar.profile.ts @@ -0,0 +1,167 @@ +import { + condition, + ignore, + mapFrom, + nullSubstitution, + preCondition, +} from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import type { + PascalAvatar, + PascalAvatarVm, +} from '../interfaces/avatar-pascal.interface'; +import { createAvatarPascalMetadata } from '../interfaces/avatar-pascal.interface'; +import type { + SnakeAvatar, + SnakeAvatarVm, +} from '../interfaces/avatar-snake.interface'; +import { createAvatarSnakeMetadata } from '../interfaces/avatar-snake.interface'; +import type { Avatar, AvatarVm } from '../interfaces/avatar.interface'; +import { createAvatarMetadata } from '../interfaces/avatar.interface'; + +export const FOR_SHOULD_IGNORE_PASS_CONDITION = 6; +export const FOR_SHOULD_IGNORE_FAIL_CONDITION = 5; + +export const avatarProfile: MappingProfile = (mapper) => { + createAvatarMetadata(); + createAvatarPascalMetadata(); + createAvatarSnakeMetadata(); + + mapper + .createMap('Avatar', 'AvatarVm') + .forMember( + (d) => d.url, + preCondition((s) => s.shouldIgnore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.forCondition, + condition((s) => s.shouldIgnore > 5, true) + ) + .forMember((d) => d.willBeIgnored, ignore()) + .forMember((d) => d.shouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap('Avatar', 'PascalAvatarVm') + .forMember( + (d) => d.Url, + preCondition((s) => s.shouldIgnore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.ForCondition, + condition((s) => s.shouldIgnore > 5, true) + ) + .forMember((d) => d.WillBeIgnored, ignore()) + .forMember((d) => d.ShouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap('Avatar', 'SnakeAvatarVm') + .forMember( + (d) => d.url, + preCondition((s) => s.shouldIgnore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.for_condition, + condition((s) => s.shouldIgnore > 5, true) + ) + .forMember((d) => d.will_be_ignored, ignore()) + .forMember((d) => d.should_be_substituted, nullSubstitution('sub')); +}; + +export const pascalAvatarProfile: MappingProfile = (mapper) => { + createAvatarMetadata(); + createAvatarPascalMetadata(); + createAvatarSnakeMetadata(); + + mapper + .createMap('PascalAvatar', 'PascalAvatarVm') + .forMember( + (d) => d.Url, + preCondition((s) => s.ShouldIgnore > 5, 'default url'), + mapFrom((s) => s.Source) + ) + .forMember( + (d) => d.ForCondition, + condition((s) => s.ShouldIgnore > 5, true) + ) + .forMember((d) => d.WillBeIgnored, ignore()) + .forMember((d) => d.ShouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap('PascalAvatar', 'AvatarVm') + .forMember( + (d) => d.url, + preCondition((s) => s.ShouldIgnore > 5, 'default url'), + mapFrom((s) => s.Source) + ) + .forMember( + (d) => d.forCondition, + condition((s) => s.ShouldIgnore > 5, true) + ) + .forMember((d) => d.willBeIgnored, ignore()) + .forMember((d) => d.shouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap('PascalAvatar', 'SnakeAvatarVm') + .forMember( + (d) => d.url, + preCondition((s) => s.ShouldIgnore > 5, 'default url'), + mapFrom((s) => s.Source) + ) + .forMember( + (d) => d.for_condition, + condition((s) => s.ShouldIgnore > 5, true) + ) + .forMember((d) => d.will_be_ignored, ignore()) + .forMember((d) => d.should_be_substituted, nullSubstitution('sub')); +}; + +export const snakeAvatarProfile: MappingProfile = (mapper) => { + createAvatarMetadata(); + createAvatarPascalMetadata(); + createAvatarSnakeMetadata(); + + mapper + .createMap('SnakeAvatar', 'SnakeAvatarVm') + .forMember( + (d) => d.url, + preCondition((s) => s.should_ignore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.for_condition, + condition((s) => s.should_ignore > 5, true) + ) + .forMember((d) => d.will_be_ignored, ignore()) + .forMember((d) => d.should_be_substituted, nullSubstitution('sub')); + + mapper + .createMap('SnakeAvatar', 'AvatarVm') + .forMember( + (d) => d.url, + preCondition((s) => s.should_ignore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.forCondition, + condition((s) => s.should_ignore > 5, true) + ) + .forMember((d) => d.willBeIgnored, ignore()) + .forMember((d) => d.shouldBeSubstituted, nullSubstitution('sub')); + + mapper + .createMap('SnakeAvatar', 'PascalAvatarVm') + .forMember( + (d) => d.Url, + preCondition((s) => s.should_ignore > 5, 'default url'), + mapFrom((s) => s.source) + ) + .forMember( + (d) => d.ForCondition, + condition((s) => s.should_ignore > 5, true) + ) + .forMember((d) => d.WillBeIgnored, ignore()) + .forMember((d) => d.ShouldBeSubstituted, nullSubstitution('sub')); +}; diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/profiles/simple-inheritance-foo.profile.ts b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/simple-inheritance-foo.profile.ts new file mode 100644 index 000000000..896ced307 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/simple-inheritance-foo.profile.ts @@ -0,0 +1,38 @@ +import { mapFrom } from '@automapper/core'; +import { MappingProfile } from '@automapper/types'; +import type { + Foo, + FooFoo, + FooFooFoo, + FooFooFooVm, + FooFooVm, + FooVm, +} from '../interfaces/simple-inheritance-foo.interface'; +import { createSimpleInheritanceFooMetadata } from '../interfaces/simple-inheritance-foo.interface'; + +export const simpleInheritanceFooProfile: MappingProfile = (mapper) => { + createSimpleInheritanceFooMetadata(); + + mapper.createMap('Foo', 'FooVm').forMember( + (d) => d.fooVm, + mapFrom((s) => s.foo) + ); + + mapper + .createMap('FooFoo', 'FooFooVm', { + extends: [mapper.getMapping('Foo', 'FooVm')], + }) + .forMember( + (d) => d.fooFooVm, + mapFrom((s) => s.fooFoo) + ); + + mapper + .createMap('FooFooFoo', 'FooFooFooVm', { + extends: [mapper.getMapping('FooFoo', 'FooFooVm')], + }) + .forMember( + (d) => d.fooFooFooVm, + mapFrom((s) => s.fooFooFoo) + ); +}; diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/profiles/simple-user.profile.ts b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/simple-user.profile.ts new file mode 100644 index 000000000..063104077 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/simple-user.profile.ts @@ -0,0 +1,28 @@ +import { mapFrom } from '@automapper/core'; +import type { MapAction, Mapper, MappingProfile } from '@automapper/types'; +import { + createSimpleUserMetadata, + SimpleUser, + SimpleUserVm, +} from '../interfaces/simple-user.interface'; + +export const simpleUserProfileFactory = (actions?: { + beforeMap: MapAction; + afterMap: MapAction; +}): MappingProfile => (mapper: Mapper) => { + createSimpleUserMetadata(); + + const fluent = mapper.createMap( + 'SimpleUser', + 'SimpleUserVm' + ); + + if (actions) { + fluent.beforeMap(actions.beforeMap).afterMap(actions.afterMap); + } + + fluent.forMember( + (d) => d.fullName, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); +}; diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/profiles/user-profile.profile.ts b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/user-profile.profile.ts new file mode 100644 index 000000000..53d2b9e94 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/user-profile.profile.ts @@ -0,0 +1,195 @@ +import { convertUsing, mapWith } from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import { dateToStringConverter } from '../converters/date-to-string.converter'; +import type { + PascalUserProfile, + PascalUserProfileVm, +} from '../interfaces/user-profile-pascal.interface'; +import { createUserProfilePascalMetadata } from '../interfaces/user-profile-pascal.interface'; +import type { + SnakeUserProfile, + SnakeUserProfileVm, +} from '../interfaces/user-profile-snake.interface'; +import { createUserProfileSnakeMetadata } from '../interfaces/user-profile-snake.interface'; +import type { + UserProfile, + UserProfileVm, +} from '../interfaces/user-profile.interface'; +import { createUserProfileMetadata } from '../interfaces/user-profile.interface'; + +export const userProfileProfile: MappingProfile = (mapper) => { + createUserProfileMetadata(); + createUserProfilePascalMetadata(); + createUserProfileSnakeMetadata(); + + mapper + .createMap('UserProfile', 'UserProfileVm') + .forMember( + (d) => d.avatar, + mapWith( + () => 'AvatarVm', + (s) => s.avatar, + () => 'Avatar' + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap( + 'UserProfile', + 'PascalUserProfileVm' + ) + .forMember( + (d) => d.Avatar, + mapWith( + () => 'PascalAvatarVm', + (s) => s.avatar, + () => 'Avatar' + ) + ) + .forMember( + (d) => d.Birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap( + 'UserProfile', + 'SnakeUserProfileVm' + ) + .forMember( + (d) => d.avatar, + mapWith( + () => 'SnakeAvatarVm', + (s) => s.avatar, + () => 'Avatar' + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); +}; + +export const pascalUserProfileProfile: MappingProfile = (mapper) => { + createUserProfileMetadata(); + createUserProfilePascalMetadata(); + createUserProfileSnakeMetadata(); + + mapper + .createMap( + 'PascalUserProfile', + 'PascalUserProfileVm' + ) + .forMember( + (d) => d.Avatar, + mapWith( + () => 'PascalAvatarVm', + (s) => s.Avatar, + () => 'PascalAvatar' + ) + ) + .forMember( + (d) => d.Birthday, + convertUsing(dateToStringConverter, (s) => s.Birthday) + ); + + mapper + .createMap( + 'PascalUserProfile', + 'UserProfileVm' + ) + .forMember( + (d) => d.avatar, + mapWith( + () => 'AvatarVm', + (s) => s.Avatar, + () => 'PascalAvatar' + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.Birthday) + ); + + mapper + .createMap( + 'PascalUserProfile', + 'SnakeUserProfileVm' + ) + .forMember( + (d) => d.avatar, + mapWith( + () => 'SnakeAvatarVm', + (s) => s.Avatar, + () => 'PascalAvatar' + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.Birthday) + ); +}; + +export const snakeUserProfileProfile: MappingProfile = (mapper) => { + createUserProfileMetadata(); + createUserProfilePascalMetadata(); + createUserProfileSnakeMetadata(); + + mapper + .createMap( + 'SnakeUserProfile', + 'SnakeUserProfileVm' + ) + .forMember( + (d) => d.avatar, + mapWith( + () => 'SnakeAvatarVm', + (s) => s.avatar, + () => 'SnakeAvatar' + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap( + 'SnakeUserProfile', + 'UserProfileVm' + ) + .forMember( + (d) => d.avatar, + mapWith( + () => 'AvatarVm', + (s) => s.avatar, + () => 'SnakeAvatar' + ) + ) + .forMember( + (d) => d.birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); + + mapper + .createMap( + 'SnakeUserProfile', + 'PascalUserProfileVm' + ) + .forMember( + (d) => d.Avatar, + mapWith( + () => 'PascalAvatarVm', + (s) => s.avatar, + () => 'SnakeAvatar' + ) + ) + .forMember( + (d) => d.Birthday, + convertUsing(dateToStringConverter, (s) => s.birthday) + ); +}; diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/profiles/user.profile.ts b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/user.profile.ts new file mode 100644 index 000000000..025ec4cf5 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/user.profile.ts @@ -0,0 +1,167 @@ +import { mapFrom } from '@automapper/core'; +import type { MappingProfile } from '@automapper/types'; +import type { + PascalUser, + PascalUserVm, +} from '../interfaces/user-pascal.interface'; +import { createUserPascalMetadata } from '../interfaces/user-pascal.interface'; +import type { + SnakeUser, + SnakeUserVm, +} from '../interfaces/user-snake.interface'; +import { createUserSnakeMetadata } from '../interfaces/user-snake.interface'; +import type { User, UserVm } from '../interfaces/user.interface'; +import { createUserMetadata } from '../interfaces/user.interface'; + +export const userProfile: MappingProfile = (mapper) => { + createUserMetadata(); + createUserPascalMetadata(); + createUserSnakeMetadata(); + + mapper + .createMap('User', 'UserVm') + .forMember( + (d) => d.first, + mapFrom((s) => s.firstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.lastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); + + mapper + .createMap('User', 'PascalUserVm') + .forMember( + (d) => d.First, + mapFrom((s) => s.firstName) + ) + .forMember( + (d) => d.Last, + mapFrom((s) => s.lastName) + ) + .forMember( + (d) => d.Full, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); + + mapper + .createMap('User', 'SnakeUserVm') + .forMember( + (d) => d.first, + mapFrom((s) => s.firstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.lastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.firstName + ' ' + s.lastName) + ); +}; + +export const pascalUserProfile: MappingProfile = (mapper) => { + createUserMetadata(); + createUserPascalMetadata(); + createUserSnakeMetadata(); + + mapper + .createMap('PascalUser', 'PascalUserVm') + .forMember( + (d) => d.First, + mapFrom((s) => s.FirstName) + ) + .forMember( + (d) => d.Last, + mapFrom((s) => s.LastName) + ) + .forMember( + (d) => d.Full, + mapFrom((s) => s.FirstName + ' ' + s.LastName) + ); + + mapper + .createMap('PascalUser', 'UserVm') + .forMember( + (d) => d.first, + mapFrom((s) => s.FirstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.LastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.FirstName + ' ' + s.LastName) + ); + + mapper + .createMap('PascalUser', 'SnakeUserVm') + .forMember( + (d) => d.first, + mapFrom((s) => s.FirstName) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.LastName) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.FirstName + ' ' + s.LastName) + ); +}; + +export const snakeUserProfile: MappingProfile = (mapper) => { + createUserMetadata(); + createUserPascalMetadata(); + createUserSnakeMetadata(); + + mapper + .createMap('SnakeUser', 'SnakeUserVm') + .forMember( + (d) => d.first, + mapFrom((s) => s.first_name) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.last_name) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.first_name + ' ' + s.last_name) + ); + + mapper + .createMap('SnakeUser', 'UserVm') + .forMember( + (d) => d.first, + mapFrom((s) => s.first_name) + ) + .forMember( + (d) => d.last, + mapFrom((s) => s.last_name) + ) + .forMember( + (d) => d.full, + mapFrom((s) => s.first_name + ' ' + s.last_name) + ); + + mapper + .createMap('SnakeUser', 'PascalUserVm') + .forMember( + (d) => d.First, + mapFrom((s) => s.first_name) + ) + .forMember( + (d) => d.Last, + mapFrom((s) => s.last_name) + ) + .forMember( + (d) => d.Full, + mapFrom((s) => s.first_name + ' ' + s.last_name) + ); +}; diff --git a/packages/integration-test/src/lib/with-pojos/inheritance.spec.ts b/packages/integration-test/src/lib/with-pojos/inheritance.spec.ts new file mode 100644 index 000000000..c69a932da --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/inheritance.spec.ts @@ -0,0 +1,46 @@ +import { setupPojos } from '../setup.spec'; +import { + FooFoo, + FooFooFoo, + FooFooFooVm, + FooFooVm, +} from './fixtures/interfaces/simple-inheritance-foo.interface'; +import { simpleInheritanceFooProfile } from './fixtures/profiles/simple-inheritance-foo.profile'; + +describe('Inheritance', () => { + const [mapper] = setupPojos('inheritance'); + + it('should work with one level inheritance', () => { + mapper.addProfile(simpleInheritanceFooProfile); + + const foo = { + foo: 'foo1', + fooFoo: 'foo2', + } as FooFoo; + + const vm = mapper.map(foo, 'FooFooVm', 'FooFoo'); + expect(vm).toBeTruthy(); + expect(vm.fooVm).toEqual(foo.foo); + expect(vm.fooFooVm).toEqual(foo.fooFoo); + }); + + it('should work with two level inheritance', () => { + mapper.addProfile(simpleInheritanceFooProfile); + + const foo = { + foo: 'foo1', + fooFoo: 'foo2', + fooFooFoo: 'foo3', + } as FooFooFoo; + + const vm = mapper.map( + foo, + 'FooFooFooVm', + 'FooFooFoo' + ); + expect(vm).toBeTruthy(); + expect(vm.fooVm).toEqual(foo.foo); + expect(vm.fooFooVm).toEqual(foo.fooFoo); + expect(vm.fooFooFooVm).toEqual(foo.fooFooFoo); + }); +}); diff --git a/packages/integration-test/src/lib/with-pojos/map-action.spec.ts b/packages/integration-test/src/lib/with-pojos/map-action.spec.ts new file mode 100644 index 000000000..c0375e263 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/map-action.spec.ts @@ -0,0 +1,173 @@ +import { setupPojos } from '../setup.spec'; +import { + SimpleUser, + SimpleUserVm, +} from './fixtures/interfaces/simple-user.interface'; +import { simpleUserProfileFactory } from './fixtures/profiles/simple-user.profile'; + +describe('MapActions', () => { + const [mapper] = setupPojos('mapActions'); + + it('should map with mapping actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile( + simpleUserProfileFactory({ + beforeMap: beforeAction, + afterMap: afterAction, + }) + ); + + const simpleUser = { + firstName: 'Chau', + lastName: 'Tran', + } as SimpleUser; + + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + + const vm = mapper.map( + simpleUser, + 'SimpleUserVm', + 'SimpleUser' + ); + assertVm(simpleUser, vm); + + expect(beforeAction).toHaveBeenCalled(); + expect(afterAction).toHaveBeenCalled(); + }); + + it('should map with map actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile(simpleUserProfileFactory()); + + const simpleUser = { + firstName: 'Chau', + lastName: 'Tran', + } as SimpleUser; + + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + + const vm = mapper.map( + simpleUser, + 'SimpleUserVm', + 'SimpleUser', + { + beforeMap: beforeAction, + afterMap: afterAction, + } + ); + assertVm(simpleUser, vm); + + expect(beforeAction).toHaveBeenCalled(); + expect(afterAction).toHaveBeenCalled(); + }); + + it('should map actions override mapping actions', () => { + const mappingBeforeAction = jest.fn(); + const mappingAfterAction = jest.fn(); + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile( + simpleUserProfileFactory({ + beforeMap: mappingBeforeAction, + afterMap: mappingAfterAction, + }) + ); + + const simpleUser = { + firstName: 'Chau', + lastName: 'Tran', + } as SimpleUser; + + expect(mappingBeforeAction).not.toHaveBeenCalled(); + expect(mappingAfterAction).not.toHaveBeenCalled(); + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + + const vm = mapper.map( + simpleUser, + 'SimpleUserVm', + 'SimpleUser', + { + beforeMap: beforeAction, + afterMap: afterAction, + } + ); + assertVm(simpleUser, vm); + + // map actions should override mapping actions + expect(mappingBeforeAction).not.toHaveBeenCalled(); + expect(mappingAfterAction).not.toHaveBeenCalled(); + expect(beforeAction).toHaveBeenCalled(); + expect(afterAction).toHaveBeenCalled(); + }); + + it('should mapArray skip mapping actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile( + simpleUserProfileFactory({ + beforeMap: beforeAction, + afterMap: afterAction, + }) + ); + + const simpleUser = { + firstName: 'Chau', + lastName: 'Tran', + } as SimpleUser; + + const vms = mapper.mapArray( + [simpleUser], + 'SimpleUserVm', + 'SimpleUser' + ); + vms.forEach((vm) => { + assertVm(simpleUser, vm); + }); + + expect(beforeAction).not.toHaveBeenCalled(); + expect(afterAction).not.toHaveBeenCalled(); + }); + + it('should mapArray allow for map actions', () => { + const beforeAction = jest.fn(); + const afterAction = jest.fn(); + + mapper.addProfile(simpleUserProfileFactory()); + + const simpleUser = { + firstName: 'Chau', + lastName: 'Tran', + } as SimpleUser; + + const vms = mapper.mapArray( + [simpleUser], + 'SimpleUserVm', + 'SimpleUser', + { + beforeMap: beforeAction, + afterMap: afterAction, + } + ); + vms.forEach((vm) => { + assertVm(simpleUser, vm); + }); + + expect(beforeAction).toHaveBeenCalledWith([simpleUser], []); + expect(afterAction).toHaveBeenCalledWith([simpleUser], vms); + }); + + function assertVm(user: SimpleUser, vm: SimpleUserVm) { + expect(vm.firstName).toEqual(user.firstName); + expect(vm.lastName).toEqual(user.lastName); + expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); + } +}); diff --git a/packages/integration-test/src/lib/with-pojos/map.spec.ts b/packages/integration-test/src/lib/with-pojos/map.spec.ts new file mode 100644 index 000000000..2fe1cf063 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/map.spec.ts @@ -0,0 +1,213 @@ +import { setupPojos } from '../setup.spec'; +import { + PascalUser, + PascalUserVm, +} from './fixtures/interfaces/user-pascal.interface'; +import { User, UserVm } from './fixtures/interfaces/user.interface'; +import { + addressProfile, + pascalAddressProfile, +} from './fixtures/profiles/address.profile'; +import { + avatarProfile, + FOR_SHOULD_IGNORE_FAIL_CONDITION, + FOR_SHOULD_IGNORE_PASS_CONDITION, + pascalAvatarProfile, +} from './fixtures/profiles/avatar.profile'; +import { + pascalUserProfileProfile, + userProfileProfile, +} from './fixtures/profiles/user-profile.profile'; +import { + pascalUserProfile, + userProfile, +} from './fixtures/profiles/user.profile'; +import { getPascalUser, getUser } from './utils/get-user'; + +describe('Map - Non Flattening', () => { + const [mapper] = setupPojos('map'); + + it('should map properly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + + const vm = mapper.map(user, 'UserVm', 'User'); + assertVm(user, vm); + }); + + it('should map correctly with condition, preCondition, and nullSubstitution', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_FAIL_CONDITION, + shouldBeSubstituted: 'will not sub', + forCondition: false, + }, + }); + + const vm = mapper.map(user, 'UserVm', 'User'); + assertVm(user, vm, { shouldIgnorePassCondition: false, shouldSub: false }); + }); + + it('should mapArray correctly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + const vms = mapper.mapArray([user, user], 'UserVm', 'User'); + expect(vms.length).toEqual(2); + vms.forEach((vm) => { + assertVm(user, vm); + }); + }); + + it('should mapAsync correctly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + + mapper.mapAsync(user, 'UserVm', 'User').then((vm) => { + assertVm(user, vm); + }); + }); + + it('should mapArrayAsync correctly', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser({ + avatar: { + shouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + shouldBeSubstituted: null, + forCondition: true, + }, + }); + mapper + .mapArrayAsync([user, user], 'UserVm', 'User') + .then((vms) => { + expect(vms.length).toEqual(2); + vms.forEach((vm) => { + assertVm(user, vm); + }); + }); + }); + + it('should throw error when map without mapping', () => { + const user = getUser(); + expect(() => mapper.map(user, 'UserVm', 'User')).toThrow(); + }); + + it('should return empty array when mapArray with empty array', () => { + const vms = mapper.mapArray([], 'UserVm', 'User'); + expect(vms).toEqual([]); + }); + + it('should map with a different casing', () => { + mapper + .addProfile(pascalAddressProfile) + .addProfile(pascalAvatarProfile) + .addProfile(pascalUserProfileProfile) + .addProfile(pascalUserProfile); + + const pascalUser = getPascalUser({ + avatar: { + ShouldIgnore: FOR_SHOULD_IGNORE_PASS_CONDITION, + ShouldBeSubstituted: null, + ForCondition: true, + }, + }); + + const vm = mapper.map( + pascalUser, + 'PascalUserVm', + 'PascalUser' + ); + expect(vm).toBeTruthy(); + }); + + function assertVm( + user: User, + vm: UserVm, + assertionPaths: { + shouldIgnorePassCondition: boolean; + shouldSub: boolean; + } = { shouldIgnorePassCondition: true, shouldSub: true } + ) { + const { shouldIgnorePassCondition, shouldSub } = assertionPaths; + + expect(vm.first).toEqual(user.firstName); + expect(vm.last).toEqual(user.lastName); + expect(vm.full).toEqual(user.firstName + ' ' + user.lastName); + + expect(vm.profile.birthday).toEqual(user.profile.birthday.toDateString()); + expect(vm.profile.bio).toEqual(user.profile.bio); + + if (shouldIgnorePassCondition) { + expect(vm.profile.avatar.url).toEqual(user.profile.avatar.source); + expect(vm.profile.avatar.forCondition).toEqual( + user.profile.avatar.forCondition + ); + } else { + expect(vm.profile.avatar.url).toEqual('default url'); + expect(vm.profile.avatar.forCondition).toEqual(true); + } + + if (shouldSub) { + expect(vm.profile.avatar.shouldBeSubstituted).toEqual('sub'); + } else { + expect(vm.profile.avatar.shouldBeSubstituted).toEqual( + vm.profile.avatar.shouldBeSubstituted + ); + } + + expect(vm.profile.avatar.willBeIgnored).not.toBeTruthy(); + + user.profile.addresses.forEach((address, index) => { + expect(vm.profile.addresses[index].formattedAddress).toEqual( + `${address.street} ${address.city} ${address.state}` + ); + }); + + // no flattening + expect(vm.jobTitle).toBeUndefined(); + expect(vm.jobAnnualSalary).toBeUndefined(); + } +}); diff --git a/packages/integration-test/src/lib/with-pojos/naming-conventions.spec.ts b/packages/integration-test/src/lib/with-pojos/naming-conventions.spec.ts new file mode 100644 index 000000000..3a41b8482 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/naming-conventions.spec.ts @@ -0,0 +1,421 @@ +import { + CamelCaseNamingConvention, + PascalCaseNamingConvention, + SnakeCaseNamingConvention, +} from '@automapper/core'; +import { setupPojos } from '../setup.spec'; +import { + createSimpleFooBarPascalMetadata, + PascalSimpleBar, + PascalSimpleBarVm, + PascalSimpleFoo, + PascalSimpleFooVm, +} from './fixtures/interfaces/simple-foo-bar-pascal.interface'; +import { + createSimpleFooBarSnakeMetadata, + SnakeSimpleBar, + SnakeSimpleBarVm, + SnakeSimpleFoo, + SnakeSimpleFooVm, +} from './fixtures/interfaces/simple-foo-bar-snake.interface'; +import { + createSimpleFooBarMetadata, + SimpleBar, + SimpleBarVm, + SimpleFoo, + SimpleFooVm, +} from './fixtures/interfaces/simple-foo-bar.interface'; +import { + PascalUser, + PascalUserVm, +} from './fixtures/interfaces/user-pascal.interface'; +import { + SnakeUser, + SnakeUserVm, +} from './fixtures/interfaces/user-snake.interface'; +import { User, UserVm } from './fixtures/interfaces/user.interface'; +import { + addressProfile, + pascalAddressProfile, + snakeAddressProfile, +} from './fixtures/profiles/address.profile'; +import { + avatarProfile, + pascalAvatarProfile, + snakeAvatarProfile, +} from './fixtures/profiles/avatar.profile'; +import { + pascalUserProfileProfile, + snakeUserProfileProfile, + userProfileProfile, +} from './fixtures/profiles/user-profile.profile'; +import { + pascalUserProfile, + snakeUserProfile, + userProfile, +} from './fixtures/profiles/user.profile'; +import { getPascalUser, getSnakeUser, getUser } from './utils/get-user'; + +describe('Naming Conventions', () => { + describe('with pascal <-> camel', () => { + const [mapper] = setupPojos('pascalCamel', { + source: new PascalCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + createSimpleFooBarMetadata(); + createSimpleFooBarPascalMetadata(); + createSimpleFooBarSnakeMetadata(); + + mapper.createMap( + 'PascalSimpleBar', + 'SimpleBarVm' + ); + mapper.createMap( + 'PascalSimpleFoo', + 'SimpleFooVm' + ); + + const foo = { + Foo: 'Foo', + FooBar: 123, + Bar: { + Bar: 'Bar', + }, + } as PascalSimpleFoo; + + const vm = mapper.map( + foo, + 'SimpleFooVm', + 'PascalSimpleFoo' + ); + expect(vm.foo).toEqual(foo.Foo); + expect(vm.bar.bar).toEqual(foo.Bar.Bar); + expect(vm.fooBar).toEqual(foo.FooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(pascalAddressProfile) + .addProfile(pascalAvatarProfile) + .addProfile(pascalUserProfileProfile) + .addProfile(pascalUserProfile); + + const user = getPascalUser(); + + const vm = mapper.map(user, 'UserVm', 'PascalUser'); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.jobTitle).toEqual(user.Job.Title); + expect(vm.jobAnnualSalary).toEqual(user.Job.AnnualSalary); + }); + }); + + describe('with camel <-> pascal', () => { + const [mapper] = setupPojos('camelPascal', { + source: new CamelCaseNamingConvention(), + destination: new PascalCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + createSimpleFooBarMetadata(); + createSimpleFooBarPascalMetadata(); + createSimpleFooBarSnakeMetadata(); + + mapper.createMap( + 'SimpleBar', + 'PascalSimpleBarVm' + ); + mapper.createMap( + 'SimpleFoo', + 'PascalSimpleFooVm' + ); + + const foo = { + foo: 'Foo', + fooBar: 123, + bar: { + bar: 'Bar', + }, + } as SimpleFoo; + + const vm = mapper.map( + foo, + 'PascalSimpleFooVm', + 'SimpleFoo' + ); + expect(vm.Foo).toEqual(foo.foo); + expect(vm.Bar.Bar).toEqual(foo.bar.bar); + expect(vm.FooBar).toEqual(foo.fooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser(); + + const vm = mapper.map(user, 'PascalUserVm', 'User'); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.JobTitle).toEqual(user.job.title); + expect(vm.JobAnnualSalary).toEqual(user.job.annualSalary); + }); + }); + + describe('with snake <-> camel', () => { + const [mapper] = setupPojos('snakeCamel', { + source: new SnakeCaseNamingConvention(), + destination: new CamelCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + createSimpleFooBarMetadata(); + createSimpleFooBarPascalMetadata(); + createSimpleFooBarSnakeMetadata(); + + mapper.createMap( + 'SnakeSimpleBar', + 'SimpleBarVm' + ); + mapper.createMap( + 'SnakeSimpleFoo', + 'SimpleFooVm' + ); + + const foo = { + foo: 'Foo', + foo_bar: 123, + bar: { + bar: 'Bar', + }, + } as SnakeSimpleFoo; + + const vm = mapper.map( + foo, + 'SimpleFooVm', + 'SnakeSimpleFoo' + ); + expect(vm.foo).toEqual(foo.foo); + expect(vm.bar.bar).toEqual(foo.bar.bar); + expect(vm.fooBar).toEqual(foo.foo_bar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(snakeAddressProfile) + .addProfile(snakeAvatarProfile) + .addProfile(snakeUserProfileProfile) + .addProfile(snakeUserProfile); + + const user = getSnakeUser(); + + const vm = mapper.map(user, 'UserVm', 'SnakeUser'); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.jobTitle).toEqual(user.job.title); + expect(vm.jobAnnualSalary).toEqual(user.job.annual_salary); + }); + }); + + describe('with camel <-> snake', () => { + const [mapper] = setupPojos('camelSnake', { + source: new CamelCaseNamingConvention(), + destination: new SnakeCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + createSimpleFooBarMetadata(); + createSimpleFooBarPascalMetadata(); + createSimpleFooBarSnakeMetadata(); + + mapper.createMap( + 'SimpleBar', + 'SnakeSimpleBarVm' + ); + mapper.createMap( + 'SimpleFoo', + 'SnakeSimpleFooVm' + ); + + const foo = { + foo: 'Foo', + fooBar: 123, + bar: { + bar: 'Bar', + }, + } as SimpleFoo; + + const vm = mapper.map( + foo, + 'SnakeSimpleFooVm', + 'SimpleFoo' + ); + expect(vm.foo).toEqual(foo.foo); + expect(vm.bar.bar).toEqual(foo.bar.bar); + expect(vm.foo_bar).toEqual(foo.fooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(addressProfile) + .addProfile(avatarProfile) + .addProfile(userProfileProfile) + .addProfile(userProfile); + + const user = getUser(); + + const vm = mapper.map(user, 'SnakeUserVm', 'User'); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.job_title).toEqual(user.job.title); + expect(vm.job_annual_salary).toEqual(user.job.annualSalary); + }); + }); + + describe('with pascal <-> snake', () => { + const [mapper] = setupPojos('pascalSnake', { + source: new PascalCaseNamingConvention(), + destination: new SnakeCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + createSimpleFooBarMetadata(); + createSimpleFooBarPascalMetadata(); + createSimpleFooBarSnakeMetadata(); + + mapper.createMap( + 'PascalSimpleBar', + 'SnakeSimpleBarVm' + ); + mapper.createMap( + 'PascalSimpleFoo', + 'SnakeSimpleFooVm' + ); + + const foo = { + Foo: 'Foo', + FooBar: 123, + Bar: { + Bar: 'Bar', + }, + } as PascalSimpleFoo; + + const vm = mapper.map( + foo, + 'SnakeSimpleFooVm', + 'PascalSimpleFoo' + ); + expect(vm.foo).toEqual(foo.Foo); + expect(vm.bar.bar).toEqual(foo.Bar.Bar); + expect(vm.foo_bar).toEqual(foo.FooBar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(pascalAddressProfile) + .addProfile(pascalAvatarProfile) + .addProfile(pascalUserProfileProfile) + .addProfile(pascalUserProfile); + + const user = getPascalUser(); + + const vm = mapper.map( + user, + 'SnakeUserVm', + 'PascalUser' + ); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.job_title).toEqual(user.Job.Title); + expect(vm.job_annual_salary).toEqual(user.Job.AnnualSalary); + }); + }); + + describe('with snake <-> pascal', () => { + const [mapper] = setupPojos('snakePascal', { + source: new SnakeCaseNamingConvention(), + destination: new PascalCaseNamingConvention(), + }); + + it('should create mapper', () => { + expect(mapper).toBeTruthy(); + }); + + it('should map', () => { + createSimpleFooBarMetadata(); + createSimpleFooBarPascalMetadata(); + createSimpleFooBarSnakeMetadata(); + + mapper.createMap( + 'SnakeSimpleBar', + 'PascalSimpleBarVm' + ); + mapper.createMap( + 'SnakeSimpleFoo', + 'PascalSimpleFooVm' + ); + + const foo = { + foo: 'Foo', + foo_bar: 123, + bar: { + bar: 'Bar', + }, + } as SnakeSimpleFoo; + + const vm = mapper.map( + foo, + 'PascalSimpleFooVm', + 'SnakeSimpleFoo' + ); + expect(vm.Foo).toEqual(foo.foo); + expect(vm.Bar.Bar).toEqual(foo.bar.bar); + expect(vm.FooBar).toEqual(foo.foo_bar); + }); + + it('should map with complex models', () => { + mapper + .addProfile(snakeAddressProfile) + .addProfile(snakeAvatarProfile) + .addProfile(snakeUserProfileProfile) + .addProfile(snakeUserProfile); + + const user = getSnakeUser(); + + const vm = mapper.map( + user, + 'PascalUserVm', + 'SnakeUser' + ); + expect(vm).toBeTruthy(); + // Asserting the whole VM is too repetitive + expect(vm.JobTitle).toEqual(user.job.title); + expect(vm.JobAnnualSalary).toEqual(user.job.annual_salary); + }); + }); +}); diff --git a/packages/integration-test/src/lib/with-pojos/utils/get-user.ts b/packages/integration-test/src/lib/with-pojos/utils/get-user.ts new file mode 100644 index 000000000..e5954a0a2 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/utils/get-user.ts @@ -0,0 +1,162 @@ +import type { PascalAddress } from '../fixtures/interfaces/address-pascal.interface'; +import type { SnakeAddress } from '../fixtures/interfaces/address-snake.interface'; +import type { Address } from '../fixtures/interfaces/address.interface'; +import type { PascalAvatar } from '../fixtures/interfaces/avatar-pascal.interface'; +import type { SnakeAvatar } from '../fixtures/interfaces/avatar-snake.interface'; +import type { Avatar } from '../fixtures/interfaces/avatar.interface'; +import type { PascalJob } from '../fixtures/interfaces/job-pascal.interface'; +import type { SnakeJob } from '../fixtures/interfaces/job-snake.interface'; +import type { Job } from '../fixtures/interfaces/job.interface'; +import type { PascalUser } from '../fixtures/interfaces/user-pascal.interface'; +import type { PascalUserProfile } from '../fixtures/interfaces/user-profile-pascal.interface'; +import type { SnakeUserProfile } from '../fixtures/interfaces/user-profile-snake.interface'; +import type { UserProfile } from '../fixtures/interfaces/user-profile.interface'; +import type { SnakeUser } from '../fixtures/interfaces/user-snake.interface'; +import type { User } from '../fixtures/interfaces/user.interface'; + +export function getUser( + partials: { + user?: Partial; + job?: Partial; + profile?: Partial; + avatar?: Partial; + } = {} +) { + const address1: Address = { + street: '123 Acme Dr', + city: 'Sim', + state: 'Show Me', + }; + + const address2: Address = { + street: '456 Rubik Dr', + city: 'Some', + state: 'October', + }; + + const avatar = { + source: 'Internet', + url: 'url.com', + ...(partials.avatar ?? {}), + } as Avatar; + + const userProfile: UserProfile = { + bio: 'Introvert-ish', + birthday: new Date('10/14/1991'), + addresses: [address1, address2], + avatar, + ...(partials.profile ?? {}), + }; + + const userJob = { + title: 'Developer', + annualSalary: 99999, + ...(partials.job ?? {}), + } as Job; + + return { + firstName: 'Chau', + lastName: 'Tran', + job: userJob, + profile: userProfile, + ...(partials.user ?? {}), + } as User; +} + +export function getPascalUser( + partials: { + user?: Partial; + job?: Partial; + profile?: Partial; + avatar?: Partial; + } = {} +) { + const address1: PascalAddress = { + Street: '123 Acme Dr', + City: 'Sim', + State: 'Show Me', + }; + + const address2: PascalAddress = { + Street: '456 Rubik Dr', + City: 'Some', + State: 'October', + }; + + const avatar = { + Source: 'Internet', + Url: 'url.com', + ...(partials.avatar ?? {}), + } as PascalAvatar; + + const userProfile: PascalUserProfile = { + Bio: 'Introvert-ish', + Birthday: new Date('10/14/1991'), + Addresses: [address1, address2], + Avatar: avatar, + ...(partials.profile ?? {}), + }; + + const userJob = { + Title: 'Developer', + AnnualSalary: 99999, + ...(partials.job ?? {}), + } as PascalJob; + + return { + FirstName: 'Chau', + LastName: 'Tran', + Job: userJob, + Profile: userProfile, + ...(partials.user ?? {}), + } as PascalUser; +} + +export function getSnakeUser( + partials: { + user?: Partial; + job?: Partial; + profile?: Partial; + avatar?: Partial; + } = {} +) { + const address1: SnakeAddress = { + street: '123 Acme Dr', + city: 'Sim', + state: 'Show Me', + }; + + const address2: SnakeAddress = { + street: '456 Rubik Dr', + city: 'Some', + state: 'October', + }; + + const avatar = { + source: 'Internet', + url: 'url.com', + ...(partials.avatar ?? {}), + } as SnakeAvatar; + + const userProfile: SnakeUserProfile = { + bio: 'Introvert-ish', + birthday: new Date('10/14/1991'), + addresses: [address1, address2], + avatar, + ...(partials.profile ?? {}), + }; + + const userJob = { + title: 'Developer', + annual_salary: 99999, + ...(partials.job ?? {}), + } as SnakeJob; + + return { + first_name: 'Chau', + last_name: 'Tran', + job: userJob, + profile: userProfile, + ...(partials.user ?? {}), + } as SnakeUser; +} diff --git a/packages/integration-test/src/lib/with-pojos/variants.spec.ts b/packages/integration-test/src/lib/with-pojos/variants.spec.ts new file mode 100644 index 000000000..98bb61b3d --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/variants.spec.ts @@ -0,0 +1,60 @@ +import { mapFrom } from '@automapper/core'; +import { setupPojos } from '../setup.spec'; +import { + createUserVariantsMetadata, + UserWithReturnKeyword, + UserWithReturnKeywordVm, +} from './fixtures/interfaces/user-variants.interface'; + +describe('Variants', () => { + const [mapper] = setupPojos('variants'); + + it('should map with model with properties with return keyword', () => { + createUserVariantsMetadata(); + mapper + .createMap( + 'UserWithReturnKeyword', + 'UserWithReturnKeywordVm' + ) + .forMember( + (d) => { + return d.returnReturnFirst; + }, + mapFrom((s) => { + return s.returnFirstName; + }) + ) + .forMember( + function (d) { + return d.returnReturnLast; + }, + mapFrom(function (s) { + return s.returnLastName; + }) + ) + .forMember( + (d) => { + return d.returnReturnFull; + }, + mapFrom(function (s) { + return s.returnFirstName + ' ' + s.returnLastName; + }) + ); + + const user = { + returnFirstName: 'Chau', + returnLastName: 'Tran', + } as UserWithReturnKeyword; + + const vm = mapper.map( + user, + 'UserWithReturnKeywordVm', + 'UserWithReturnKeyword' + ); + expect(vm.returnReturnFirst).toEqual(user.returnFirstName); + expect(vm.returnReturnLast).toEqual(user.returnLastName); + expect(vm.returnReturnFull).toEqual( + user.returnFirstName + ' ' + user.returnLastName + ); + }); +}); diff --git a/packages/integration-test/tsconfig.json b/packages/integration-test/tsconfig.json new file mode 100644 index 000000000..62ebbd946 --- /dev/null +++ b/packages/integration-test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/models/tsconfig.lib.json b/packages/integration-test/tsconfig.lib.json similarity index 100% rename from packages/models/tsconfig.lib.json rename to packages/integration-test/tsconfig.lib.json diff --git a/packages/integration-test/tsconfig.spec.json b/packages/integration-test/tsconfig.spec.json new file mode 100644 index 000000000..559410b96 --- /dev/null +++ b/packages/integration-test/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/packages/models/package.json b/packages/models/package.json deleted file mode 100644 index 860f08d11..000000000 --- a/packages/models/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@automapper/models", - "version": "0.0.1" -} diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts deleted file mode 100644 index 20c4b92c0..000000000 --- a/packages/models/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/utils'; -export * from './lib/core'; diff --git a/packages/models/src/lib/core/create-map-options.ts b/packages/models/src/lib/core/create-map-options.ts deleted file mode 100644 index 323a02262..000000000 --- a/packages/models/src/lib/core/create-map-options.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NamingConvention } from './naming-convention'; - -export interface CreateMapOptions { - sourceNamingConvention?: NamingConvention; - destinationNamingConvention?: NamingConvention; - includeBase?: []; -} diff --git a/packages/models/src/lib/core/create-mapper-options.ts b/packages/models/src/lib/core/create-mapper-options.ts deleted file mode 100644 index 1a72e8a9a..000000000 --- a/packages/models/src/lib/core/create-mapper-options.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NamingConvention } from './naming-convention'; - -export interface CreateMapperOptions { - /** - * NamingConvention of the source object's properties - */ - sourceNamingConvention?: NamingConvention; - /** - * NamingConvention of the destination object's properties - */ - destinationNamingConvention?: NamingConvention; - /** - * Whether to skip assertion on unmapped properties or not - */ - skipUnmappedAssertion?: boolean; - /** - * Whether to throw error or just log to the console - */ - throwError?: boolean; -} diff --git a/packages/models/src/lib/core/index.ts b/packages/models/src/lib/core/index.ts deleted file mode 100644 index 7cdd66b7a..000000000 --- a/packages/models/src/lib/core/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './naming-convention'; -export * from './create-mapper-options'; -export * from './create-map-options'; -export * from './storage'; diff --git a/packages/models/src/lib/core/naming-convention.ts b/packages/models/src/lib/core/naming-convention.ts deleted file mode 100644 index aebc00f82..000000000 --- a/packages/models/src/lib/core/naming-convention.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface NamingConvention { - splittingExpression: RegExp; - separatorCharacter: string; - transformPropertyName: (sourcePropNameParts: string[]) => string; -} diff --git a/packages/models/src/lib/core/plugin.ts b/packages/models/src/lib/core/plugin.ts deleted file mode 100644 index fb0929e64..000000000 --- a/packages/models/src/lib/core/plugin.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AutoMapperStorage } from '@automapper/models'; - -export interface AutoMapperPlugin { - createMap( - storage: AutoMapperStorage<'mapping'>, - sourceKey: TMappingKey, - destinationKey: TMappingKey - ); -} diff --git a/packages/models/src/lib/core/storage.ts b/packages/models/src/lib/core/storage.ts deleted file mode 100644 index c934473de..000000000 --- a/packages/models/src/lib/core/storage.ts +++ /dev/null @@ -1,59 +0,0 @@ -export type EntryValueMap = { - mapping: WeakMap; - profile: any; - metadata: any; -}; - -export abstract class AutoMapperStorage< - TType extends 'mapping' | 'profile' | 'metadata', - TStorageKey -> { - protected abstract storage: WeakMap; - - get( - entryKey, - nestedEntryKey?: EntryValueMap[TType] extends WeakMap - ? any - : never - ): EntryValueMap[TType] { - return this.getInternal(this.storage, entryKey, nestedEntryKey); - } - - has( - entryKey, - nestedEntryKey?: EntryValueMap[TType] extends WeakMap - ? any - : never - ): boolean { - return this.hasInternal(this.storage, entryKey, nestedEntryKey); - } - - set( - entryKey, - entryValue: EntryValueMap[TType] extends WeakMap - ? [any, EntryValueMap[TType]] - : EntryValueMap[TType] - ): void { - this.setInternal(this.storage, entryKey, entryValue); - } - - protected abstract getInternal( - storage: WeakMap, - entryKey, - nestedEntryKey: EntryValueMap[TType] extends WeakMap ? any : never - ): EntryValueMap[TType]; - - protected abstract hasInternal( - storage: WeakMap, - entryKey, - nestedEntryKey: EntryValueMap[TType] extends WeakMap ? any : never - ): boolean; - - protected abstract setInternal( - storage: WeakMap, - entryKey, - entryValue: EntryValueMap[TType] extends WeakMap - ? [any, EntryValueMap[TType]] - : EntryValueMap[TType] - ): void; -} diff --git a/packages/models/src/lib/models.ts b/packages/models/src/lib/models.ts deleted file mode 100644 index b090ae5da..000000000 --- a/packages/models/src/lib/models.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function models(): string { - return 'models'; -} diff --git a/packages/models/src/lib/utils/dict.ts b/packages/models/src/lib/utils/dict.ts deleted file mode 100644 index f4ede13a2..000000000 --- a/packages/models/src/lib/utils/dict.ts +++ /dev/null @@ -1 +0,0 @@ -export type Dict = Record; diff --git a/packages/models/src/lib/utils/index.ts b/packages/models/src/lib/utils/index.ts deleted file mode 100644 index 4e0567848..000000000 --- a/packages/models/src/lib/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './unpacked'; -export * from './dict'; diff --git a/packages/models/src/lib/utils/unpacked.ts b/packages/models/src/lib/utils/unpacked.ts deleted file mode 100644 index 029f628d8..000000000 --- a/packages/models/src/lib/utils/unpacked.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type Unpacked = T extends (infer U)[] - ? U - : T extends (...args: unknown[]) => infer U - ? U - : T extends Promise - ? U - : T; diff --git a/packages/pojos/.eslintrc.json b/packages/pojos/.eslintrc.json new file mode 100644 index 000000000..f2c2a56a3 --- /dev/null +++ b/packages/pojos/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": "../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "rules": { + "@typescript-eslint/no-non-null-assertion": 0, + "no-prototype-builtins": 0 + } +} diff --git a/packages/pojos/README.md b/packages/pojos/README.md new file mode 100644 index 000000000..33ad7ecf0 --- /dev/null +++ b/packages/pojos/README.md @@ -0,0 +1,7 @@ +# pojos + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test pojos` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/pojos/jest.config.js b/packages/pojos/jest.config.js new file mode 100644 index 000000000..1426051b0 --- /dev/null +++ b/packages/pojos/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'pojos', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + }, + }, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/packages/pojos', +}; diff --git a/packages/pojos/package.json b/packages/pojos/package.json new file mode 100644 index 000000000..8401b0476 --- /dev/null +++ b/packages/pojos/package.json @@ -0,0 +1,4 @@ +{ + "name": "@automapper/pojos", + "version": "0.0.1" +} diff --git a/packages/pojos/src/index.ts b/packages/pojos/src/index.ts new file mode 100644 index 000000000..577fc3eff --- /dev/null +++ b/packages/pojos/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/pojos'; +export * from './lib/create-metadata-map'; diff --git a/packages/pojos/src/lib/create-metadata-map.ts b/packages/pojos/src/lib/create-metadata-map.ts new file mode 100644 index 000000000..ef55aa380 --- /dev/null +++ b/packages/pojos/src/lib/create-metadata-map.ts @@ -0,0 +1,56 @@ +import { pojosSymbolStorage } from './storages'; + +/** + * + * createMetadataMap('Bar', { + * baz: String + * }) + * + * createMetadataMap('Foo', { + * foo: String, + * bar: 'Bar' + * }) + */ + +export function createMetadataMap( + key: string, + metadataOrMetadataMap, + metadata? +) { + const toMergeSymbol = + typeof metadataOrMetadataMap === 'string' + ? Symbol.for(metadataOrMetadataMap) + : ''; + + metadata = + typeof metadataOrMetadataMap === 'string' + ? metadata || {} + : metadataOrMetadataMap; + + const toMergeMetadata = toMergeSymbol + ? pojosSymbolStorage.get(toMergeSymbol) + : []; + + const symbol = Symbol.for(key); + + const entries = Object.entries(metadata); + if (!entries.length) { + pojosSymbolStorage.set(symbol, toMergeMetadata); + return; + } + + const result = []; + for (const [existProp, existMeta] of toMergeMetadata) { + if (entries.some(([entryProp]) => existProp === entryProp)) { + continue; + } + + result.push([existProp, existMeta]); + } + + result.push(...entries); + pojosSymbolStorage.set( + symbol, + result.filter(([, meta]) => meta !== null) + ); +} diff --git a/packages/pojos/src/lib/pojos.spec.ts b/packages/pojos/src/lib/pojos.spec.ts new file mode 100644 index 000000000..e79ad542f --- /dev/null +++ b/packages/pojos/src/lib/pojos.spec.ts @@ -0,0 +1,7 @@ +import { pojos } from './pojos'; + +describe('pojos', () => { + it('should work', () => { + expect(pojos()).toEqual('pojos'); + }); +}); diff --git a/packages/pojos/src/lib/pojos.ts b/packages/pojos/src/lib/pojos.ts new file mode 100644 index 000000000..a608a8e8a --- /dev/null +++ b/packages/pojos/src/lib/pojos.ts @@ -0,0 +1,90 @@ +import { createInitialMapping } from '@automapper/core'; +import type { + CreateMapOptions, + ErrorHandler, + MapPlugin, +} from '@automapper/types'; +import { MappingClassId } from '@automapper/types'; +import { + PojosMappingStorage, + PojosMetadataStorage, + pojosSymbolStorage, +} from './storages'; +import { instantiate } from './utils'; + +export const pojos = (errorHandler: ErrorHandler): MapPlugin => { + const metadataStorage = new PojosMetadataStorage(); + const mappingStorage = new PojosMappingStorage(); + + return { + initializeMapping( + source: string, + destination: string, + options?: CreateMapOptions + ) { + if (mappingStorage.has(source, destination)) { + errorHandler.handle( + `Mapping for source ${source} and destination ${destination} already exists` + ); + return; + } + + exploreMetadata(metadataStorage, source, destination); + + const [destinationObj, destinationNestedMetadataMap] = instantiate( + metadataStorage, + destination + ); + + const [sourceObj, sourceNestedMetadataMap] = instantiate( + metadataStorage, + source + ); + + return createInitialMapping( + sourceObj, + destinationObj, + sourceNestedMetadataMap, + destinationNestedMetadataMap, + (mapping) => { + mappingStorage.set(source, destination, mapping); + }, + options + ); + }, + getMapping(source: string, destination: string) { + const mapping = mappingStorage.get(source, destination); + if (!mapping) { + errorHandler.handle( + `Mapping not found for source ${source} and destination ${destination}` + ); + return; + } + + const [sourceObj] = instantiate(metadataStorage, source); + const [destinationObj] = instantiate(metadataStorage, destination); + + mapping[MappingClassId.mappings] = [sourceObj, destinationObj]; + return mapping; + }, + dispose() { + metadataStorage.dispose(); + mappingStorage.dispose(); + pojosSymbolStorage.dispose(); + }, + }; +}; + +function exploreMetadata( + metadataStorage: PojosMetadataStorage, + ...keys: string[] +) { + keys.forEach((key) => { + if (!metadataStorage.has(key)) { + const metadataList = pojosSymbolStorage.get(Symbol.for(key)); + for (const [propertyKey, metadata] of metadataList) { + metadataStorage.addMetadata(key, [propertyKey, () => metadata]); + } + } + }); +} diff --git a/packages/pojos/src/lib/storages/index.ts b/packages/pojos/src/lib/storages/index.ts new file mode 100644 index 000000000..e14d12b6b --- /dev/null +++ b/packages/pojos/src/lib/storages/index.ts @@ -0,0 +1,3 @@ +export * from './pojos-mapping.storage'; +export * from './pojos-metadata.storage'; +export * from './pojos-symbol.storage'; diff --git a/packages/pojos/src/lib/storages/pojos-mapping.storage.ts b/packages/pojos/src/lib/storages/pojos-mapping.storage.ts new file mode 100644 index 000000000..ebaa72fc2 --- /dev/null +++ b/packages/pojos/src/lib/storages/pojos-mapping.storage.ts @@ -0,0 +1,36 @@ +import type { Mapping, MappingStorage } from '@automapper/types'; + +/** + * Internal PojosMappingStorage + * + * @private + */ +export class PojosMappingStorage implements MappingStorage { + private storage = new Map>(); + + get(source: string, destination: string): Mapping | undefined { + return this.storage.get(source)?.get(destination); + } + + has(source: string, destination: string): boolean { + return this.storage.get(source)?.has(destination); + } + + set(source: string, destination: string, mapping: Mapping): void { + if (!this.storage.has(source)) { + this.storage.set( + source, + new Map().set(destination, mapping) + ); + return; + } + + if (!this.has(source, destination)) { + this.storage.get(source)?.set(destination, mapping); + } + } + + dispose(): void { + this.storage = new Map>(); + } +} diff --git a/packages/pojos/src/lib/storages/pojos-metadata.storage.ts b/packages/pojos/src/lib/storages/pojos-metadata.storage.ts new file mode 100644 index 000000000..d18f5de1c --- /dev/null +++ b/packages/pojos/src/lib/storages/pojos-metadata.storage.ts @@ -0,0 +1,59 @@ +import type { Metadata, MetadataStorage } from '@automapper/types'; + +/** + * Internal PojosMetadataStorage + * + * @private + */ +export class PojosMetadataStorage implements MetadataStorage { + private storage = new Map>>(); + + addMetadata(metaKey: string, metadata: Metadata): void { + // Get metadata on the model + const exists = this.storage.get(metaKey) ?? []; + + // if already exists, break + if (exists.some(([existKey]) => existKey === metadata[0])) { + return; + } + + this.storage.set(metaKey, [...exists, metadata]); + } + + getMetadata(metaKey: string): Array> { + const metadataList = this.storage.get(metaKey) ?? []; + let i = metadataList.length; + + // empty metadata + if (!i) { + return []; + } + + const resultMetadataList: Array> = []; + while (i--) { + const metadata = metadataList[i]; + // skip existing + if (resultMetadataList.some(([metaKey]) => metaKey === metadata[0])) { + continue; + } + resultMetadataList.push(metadataList[i]); + } + + return resultMetadataList; + } + + getMetadataForKey( + metaKey: string, + key: string + ): Metadata | undefined { + return this.getMetadata(metaKey).find(([metaKey]) => metaKey === key); + } + + has(metaKey: string): boolean { + return this.storage.has(metaKey); + } + + dispose(): void { + this.storage = new Map>>(); + } +} diff --git a/packages/pojos/src/lib/storages/pojos-symbol.storage.ts b/packages/pojos/src/lib/storages/pojos-symbol.storage.ts new file mode 100644 index 000000000..6db01b838 --- /dev/null +++ b/packages/pojos/src/lib/storages/pojos-symbol.storage.ts @@ -0,0 +1,56 @@ +/** + * Internal PojosSymbolStorage + * + * @private + */ +import type { Metadata } from '@automapper/types'; +import { MetadataClassId } from '@automapper/types'; + +class PojosSymbolStorage { + private storage = new Map< + symbol, + [ + propertyKey: string, + metadata: ReturnType[MetadataClassId.metadataFn]> + ][] + >(); + + set( + key: symbol, + rawMetadataList: [ + propertyKey: string, + metadata: ReturnType[MetadataClassId.metadataFn]> + ][] + ): void { + if (this.storage.has(key)) { + return; + } + + this.storage.set(key, rawMetadataList); + } + + get( + key: symbol + ): [ + propertyKey: string, + metadata: ReturnType[MetadataClassId.metadataFn]> + ][] { + return this.storage.get(key); + } + + has(key: symbol): boolean { + return this.storage.has(key); + } + + dispose() { + this.storage = new Map< + symbol, + [ + propertyKey: string, + metadata: ReturnType[MetadataClassId.metadataFn]> + ][] + >(); + } +} + +export const pojosSymbolStorage = new PojosSymbolStorage(); diff --git a/packages/pojos/src/lib/utils/index.ts b/packages/pojos/src/lib/utils/index.ts new file mode 100644 index 000000000..7f99c0117 --- /dev/null +++ b/packages/pojos/src/lib/utils/index.ts @@ -0,0 +1,3 @@ +export * from './instantiate.util'; +export * from './is-date-constructor.util'; +export * from './is-primitive-constructor.util'; diff --git a/packages/pojos/src/lib/utils/instantiate.util.ts b/packages/pojos/src/lib/utils/instantiate.util.ts new file mode 100644 index 000000000..a78cbdf36 --- /dev/null +++ b/packages/pojos/src/lib/utils/instantiate.util.ts @@ -0,0 +1,64 @@ +import { isDefined, isEmpty } from '@automapper/core'; +import type { Dictionary } from '@automapper/types'; +import { PojosMetadataStorage } from '../storages'; +import { isDateConstructor } from './is-date-constructor.util'; +import { isPrimitiveConstructor } from './is-primitive-constructor.util'; + +export function instantiate>( + metadataStorage: PojosMetadataStorage, + model: string, + defaultValue?: TModel +): [TModel, unknown[]?] { + const metadata = metadataStorage.getMetadata(model); + const obj = Object.assign({}, defaultValue || {}) as TModel; + + if (isEmpty(metadata) || !metadata) { + return [obj]; + } + + const nestedMetadataMap = []; + let i = metadata.length; + while (i--) { + const [key, meta] = metadata[i]; + const valueAtKey = obj[key]; + const metaResult = meta(); + + if (isPrimitiveConstructor(metaResult)) { + obj[key] = isDefined(valueAtKey) ? valueAtKey : undefined; + continue; + } + if (isDateConstructor(metaResult)) { + obj[key] = isDefined(valueAtKey) ? new Date(valueAtKey) : new Date(); + continue; + } + + if (typeof metaResult !== 'string') { + continue; + } + + nestedMetadataMap.push([key, metaResult]); + if (Array.isArray(valueAtKey)) { + obj[key] = valueAtKey.map((val) => { + const [childObj] = instantiate(metadataStorage, metaResult, val); + return childObj; + }); + continue; + } + + if (isDefined(valueAtKey)) { + const [result] = instantiate(metadataStorage, metaResult, valueAtKey); + obj[key] = result; + continue; + } + + if (isDefined(defaultValue)) { + obj[key] = valueAtKey; + continue; + } + + const [result] = instantiate(metadataStorage, metaResult); + obj[key] = result; + } + + return [obj, nestedMetadataMap]; +} diff --git a/packages/pojos/src/lib/utils/is-date-constructor.util.ts b/packages/pojos/src/lib/utils/is-date-constructor.util.ts new file mode 100644 index 000000000..d189376ae --- /dev/null +++ b/packages/pojos/src/lib/utils/is-date-constructor.util.ts @@ -0,0 +1,3 @@ +export function isDateConstructor(value: unknown): boolean { + return value === Date; +} diff --git a/packages/pojos/src/lib/utils/is-primitive-constructor.util.ts b/packages/pojos/src/lib/utils/is-primitive-constructor.util.ts new file mode 100644 index 000000000..409c72c72 --- /dev/null +++ b/packages/pojos/src/lib/utils/is-primitive-constructor.util.ts @@ -0,0 +1,3 @@ +export function isPrimitiveConstructor(value: unknown): boolean { + return value === String || value === Number || value === Boolean; +} diff --git a/packages/pojos/tsconfig.json b/packages/pojos/tsconfig.json new file mode 100644 index 000000000..62ebbd946 --- /dev/null +++ b/packages/pojos/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/pojos/tsconfig.lib.json b/packages/pojos/tsconfig.lib.json new file mode 100644 index 000000000..037d796f2 --- /dev/null +++ b/packages/pojos/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/pojos/tsconfig.spec.json b/packages/pojos/tsconfig.spec.json new file mode 100644 index 000000000..559410b96 --- /dev/null +++ b/packages/pojos/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/packages/types/.eslintrc.json b/packages/types/.eslintrc.json new file mode 100644 index 000000000..d0bc09ef5 --- /dev/null +++ b/packages/types/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "extends": "../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "rules": { + "@typescript-eslint/ban-types": 0 + } +} diff --git a/packages/models/README.md b/packages/types/README.md similarity index 85% rename from packages/models/README.md rename to packages/types/README.md index 24ef2e4a1..6f403c530 100644 --- a/packages/models/README.md +++ b/packages/types/README.md @@ -1,3 +1,3 @@ -# models +# types This library was generated with [Nx](https://nx.dev). diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 000000000..43a318146 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,4 @@ +{ + "name": "@automapper/types", + "version": "0.0.1" +} diff --git a/packages/types/src/core.ts b/packages/types/src/core.ts new file mode 100644 index 000000000..f873ef489 --- /dev/null +++ b/packages/types/src/core.ts @@ -0,0 +1,573 @@ +import { TransformationType } from './enums'; +import type { CreateMapOptions, MapOptions } from './options'; +import type { + Dictionary, + Fn, + Selector, + SelectorReturn, + Unpacked, + ValueSelector, +} from './utils'; + +export interface ConditionPredicate< + TSource extends Dictionary = unknown +> { + (source: TSource): boolean; +} + +export interface MapAction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +> { + (source: TSource, destination: TDestination): void; +} + +export interface NamingConvention { + splittingExpression: RegExp; + separatorCharacter: string; + transformPropertyName: (sourcePropNameParts: string[]) => string; +} + +export interface Resolver< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TResolvedType = SelectorReturn +> { + resolve(source: TSource, destination?: TDestination): TResolvedType; +} + +export interface Converter< + TConvertSource = unknown, + TConvertDestination = unknown +> { + convert(source: TConvertSource): TConvertDestination; +} + +export type MemberMapFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> = + | MapInitializeFunction + | MapFromFunction + | MapWithFunction + | ConditionFunction + | FromValueFunction + | ConvertUsingFunction + | NullSubstitutionFunction + | MapDeferFunction + | IgnoreFunction; + +export interface PreConditionFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + (predicate: ConditionPredicate, defaultValue?: TSelectorReturn): [ + preConditionPredicate: (source: TSource) => boolean, + defaultValue?: TSelectorReturn + ]; +} + +export interface DeferFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + (source: TSource): Exclude< + ReturnType>, + ReturnType> + >; +} + +export interface MapDeferFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + (deferFn: DeferFunction): [ + type: TransformationType.MapDefer, + misc: null, + fn: DeferFunction + ]; +} + +export interface MapFromFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + ( + from: + | ValueSelector + | Resolver + ): [ + type: TransformationType.MapFrom, + misc: typeof from extends Resolver + ? null + : ValueSelector, + fn: (source: TSource, destination?: TDestination) => TSelectorReturn + ]; +} + +export interface MapWithFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + ( + withDestination: Fn>, + withSourceValue: ValueSelector, + withSource: Fn>> + ): [ + type: TransformationType.MapWith, + misc: ValueSelector, + fn: ( + sourceObj: TSource, + mapper: Mapper + ) => TSelectorReturn | undefined | null + ]; +} + +export interface ConditionFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + (predicate: ConditionPredicate, defaultValue?: TSelectorReturn): [ + type: TransformationType.Condition, + misc: null, + fn: (source: TSource, ...sourceMemberPaths: string[]) => TSelectorReturn + ]; +} + +export interface FromValueFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + (rawValue: TSelectorReturn): [ + type: TransformationType.FromValue, + misc: null, + fn: () => TSelectorReturn + ]; +} + +export interface ConvertUsingFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + ( + converter: Converter, + value?: Selector + ): [ + type: TransformationType.ConvertUsing, + misc: null, + fn: Selector + ]; +} + +export interface NullSubstitutionFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + (substitution: TSelectorReturn): [ + type: TransformationType.NullSubstitution, + misc: null, + fn: (source: TSource, ...sourceMemberPaths: string[]) => TSelectorReturn + ]; +} + +export interface IgnoreFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +> { + (): [type: TransformationType.Ignore, misc: null, fn: null]; +} + +export interface MapInitializeFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> { + (...paths: string[]): [ + type: TransformationType.MapInitialize, + misc: null, + fn: Selector + ]; +} + +export interface Mapper { + name: string; + + createMap< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + source: new (...args: unknown[]) => TSource, + destination: new (...args: unknown[]) => TDestination, + options?: CreateMapOptions + ): CreateMapFluentFunction; + + createMap< + TSource extends Dictionary, + TDestination extends Dictionary + >( + source: string, + destination: string, + options?: CreateMapOptions + ): CreateMapFluentFunction; + + createMap< + TSource extends Dictionary, + TDestination extends Dictionary + >( + source: TKey, + destination: TKey, + options?: CreateMapOptions + ): CreateMapFluentFunction; + + getMapping(source: TKey, destination: TKey): Mapping; + + getMapping< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + source: new (...args: unknown[]) => TSource, + destination: new (...args: unknown[]) => TDestination + ): Mapping; + + getMapping< + TSource extends Dictionary, + TDestination extends Dictionary + >( + source: string, + destination: string + ): Mapping; + + addProfile(profile): Mapper; + + map< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + sourceObj: TSource, + destination: new (...args: unknown[]) => TDestination, + source: new (...args: unknown[]) => TSource, + options?: MapOptions + ): TDestination; + + map< + TSource extends Dictionary, + TDestination extends Dictionary + >( + sourceObj: TSource, + destination: string, + source: string, + options?: MapOptions + ): TDestination; + + mapAsync< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + sourceObj: TSource, + destination: new (...args: unknown[]) => TDestination, + source: new (...args: unknown[]) => TSource, + options?: MapOptions + ): Promise; + + mapAsync< + TSource extends Dictionary, + TDestination extends Dictionary + >( + sourceObj: TSource, + destination: string, + source: string, + options?: MapOptions + ): Promise; + + mapArray< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + sourceArray: TSource[], + destination: new (...args: unknown[]) => TDestination, + source: new (...args: unknown[]) => TSource, + options?: MapOptions + ): TDestination[]; + + mapArray< + TSource extends Dictionary, + TDestination extends Dictionary + >( + sourceArray: TSource[], + destination: string, + source: string, + options?: MapOptions + ): TDestination[]; + + mapArrayAsync< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + sourceArray: TSource[], + destination: new (...args: unknown[]) => TDestination, + source: new (...args: unknown[]) => TSource, + options?: MapOptions + ): Promise; + + mapArrayAsync< + TSource extends Dictionary, + TDestination extends Dictionary + >( + sourceArray: TSource[], + destination: string, + source: string, + options?: MapOptions + ): Promise; + + dispose(): void; +} + +export interface CreateMapFluentFunction< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +> { + forMember>( + selector: Selector, + memberMapFunction: ReturnType< + MapFromFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + memberMapFunction: ReturnType< + MapWithFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + memberMapFunction: ReturnType< + MapWithFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + memberMapFunction: ReturnType< + ConditionFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + memberMapFunction: ReturnType< + FromValueFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + memberMapFunction: ReturnType< + ConvertUsingFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + memberMapFunction: ReturnType< + NullSubstitutionFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + memberMapFunction: ReturnType + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType< + MapFromFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType< + MapWithFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType< + MapWithFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType< + ConditionFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType< + FromValueFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType< + ConvertUsingFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType< + NullSubstitutionFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + memberMapFunction: ReturnType + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + mapDeferFunction: ReturnType< + MapDeferFunction + > + ): CreateMapFluentFunction; + + forMember>( + selector: Selector, + preConditionFunction: ReturnType< + PreConditionFunction + >, + mapDeferFunction: ReturnType< + MapDeferFunction + > + ): CreateMapFluentFunction; + + beforeMap( + action: MapAction + ): CreateMapFluentFunction; + + afterMap( + action: MapAction + ): CreateMapFluentFunction; +} + +export type MappingTransformation< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> = [ + mapFn: ReturnType>, + preCond?: ReturnType< + PreConditionFunction + > +]; +export type MappingProperty< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TSelectorReturn = SelectorReturn +> = readonly [ + paths: [target: string, origin?: string], + transformation: MappingTransformation +]; + +export type Mapping< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +> = [ + mappings: [source: TSource, destination: TDestination], + properties: Array< + [ + path: string, + mappingProperty: MappingProperty< + TSource, + TDestination, + SelectorReturn + >, + nestedMappingPair?: [unknown, unknown] + ] + >, + actions: [ + beforeMap?: MapAction, + afterMap?: MapAction + ], + namingConventions?: [source: NamingConvention, destination: NamingConvention], + bases?: [baseSource: unknown, baseDestination: unknown][] +]; + +export interface Disposable { + dispose(): void; +} + +export interface MetadataStorage extends Disposable { + getMetadata(metaKey: TKey): Array>; + + getMetadataForKey(metaKey: TKey, key: string): Metadata | undefined; + + addMetadata(metaKey: TKey, metadata: Metadata): void; + + has(metaKey: TKey): boolean; +} + +export interface MappingStorage extends Disposable { + get(source: TKey, destination: TKey): Mapping | undefined; + + set(source: TKey, destination: TKey, mapping: Mapping): void; + + has(source: TKey, destination: TKey): boolean; +} + +export type Metadata = [ + property: string, + metaTypeFn: () => String | Number | Boolean | Date | TMetaType +]; + +export interface ErrorHandler { + handle: (message: string) => void; +} + +export interface MappingProfile { + (mapper: Mapper): void; +} diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts new file mode 100644 index 000000000..7c7df5312 --- /dev/null +++ b/packages/types/src/enums.ts @@ -0,0 +1,46 @@ +export const enum TransformationType { + Ignore, + MapFrom, + Condition, + FromValue, + MapWith, + ConvertUsing, + MapInitialize, + NullSubstitution, + MapDefer, +} + +export const enum MappingClassId { + mappings, + properties, + actions, + namingConventions, + bases, +} + +export const enum MappingPropertiesClassId { + path, + property, + nestedMappingPair, +} + +export const enum MappingPropertyClassId { + paths, + transformation, +} + +export const enum MappingTransformationClassId { + mapFn, + preCond, +} + +export const enum MapFnClassId { + type, + misc, + fn, +} + +export const enum MetadataClassId { + propertyKey, + metadataFn +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 000000000..1a9491984 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,5 @@ +export * from './utils'; +export * from './core'; +export * from './options'; +export * from './enums'; +export * from './plugin'; diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts new file mode 100644 index 000000000..88cbcca04 --- /dev/null +++ b/packages/types/src/options.ts @@ -0,0 +1,37 @@ +import type { + ErrorHandler, + MapAction, + Mapping, + NamingConvention, +} from './core'; +import type { MapPlugin } from './plugin'; +import type { Dictionary } from './utils'; + +export interface MapOptions< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +> { + beforeMap?: MapAction; + afterMap?: MapAction; +} + +export interface CreateMapOptions< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown +> { + namingConventions?: { + source: NamingConvention; + destination: NamingConvention; + }; + extends?: Mapping[]; +} + +export interface CreateMapperOptions { + name: string; + namingConventions?: { + source: NamingConvention; + destination: NamingConvention; + }; + pluginInitializer: (errorHandler: ErrorHandler) => MapPlugin; + errorHandle?: ErrorHandler; +} diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts new file mode 100644 index 000000000..42d0a0638 --- /dev/null +++ b/packages/types/src/plugin.ts @@ -0,0 +1,50 @@ +import type { Mapping } from './core'; +import type { CreateMapOptions } from './options'; +import type { Dictionary } from './utils'; + +export interface MapPlugin { + /** + * Instruction on how a plugin should initialize a mapping for a pair of Source <> Destination + * This method will make use of the plugin's internal storages to store information for this pair. + * + * @param source - a key to be used to identify the information about a particular Source + * @param destination - a key to be used to identify the information about a particular Destination + * @param options {CreateMapOptions} - options for when initialize a mapping (which is globally applied to this pair of Source <> Destination) + */ + initializeMapping( + source: TKey, + destination: TKey, + options?: CreateMapOptions + ): Mapping; + + /** + * Get the Mapping for a pair of Source <> Destination + * + * @param source - a key to be used to identify the information about a particular Source + * @param destination - a key to be used to identify the information about a particular Destination + */ + getMapping(source: TKey, destination: TKey): Mapping; + + /** + * An optional pre-map function to prepare the source and destination before map + * + * @param source - a key to be used to identify the information about a particular Source + * @param destination - a key to be used to identify the information about a particular Destination + * @param sourceObj - a plain object that takes the shape of the source + * @param destinationObj - a plain object that takes the shape of the destination + */ + preMap?< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown + >( + source: TKey, + destination: TKey, + sourceObj?: TSource, + destinationObj?: TDestination + ): [sourceInstance: TSource, destinationInstance: TDestination]; + + /** + * Optional method to clean up the plugin's storages + */ + dispose?(): void; +} diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts new file mode 100644 index 000000000..b16a5ed5a --- /dev/null +++ b/packages/types/src/utils.ts @@ -0,0 +1,36 @@ +export type Unpacked = T extends (infer U)[] + ? U + : T extends (...args: unknown[]) => infer U + ? U + : T extends new (...args: unknown[]) => infer U + ? U + : T extends Promise + ? U + : T; +export type Dictionary = { [key in keyof T]?: unknown }; +export type Fn = () => T; + +export interface Selector< + TObject extends Dictionary = unknown, + TReturnType = unknown +> { + (obj: TObject): TReturnType; +} + +export type SelectorReturn> = ReturnType< + Selector +>; + +export interface ValueSelector< + TSource extends Dictionary = unknown, + TDestination extends Dictionary = unknown, + TValueReturn = SelectorReturn +> { + (source: TSource): TValueReturn; +} + +export interface TransformerMetadataFactory< + TModel extends Dictionary = unknown +> { + __NARTC_AUTOMAPPER_METADATA_FACTORY?: () => Dictionary; +} diff --git a/packages/models/tsconfig.json b/packages/types/tsconfig.json similarity index 100% rename from packages/models/tsconfig.json rename to packages/types/tsconfig.json diff --git a/packages/types/tsconfig.lib.json b/packages/types/tsconfig.lib.json new file mode 100644 index 000000000..037d796f2 --- /dev/null +++ b/packages/types/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/tools/schematics/.gitkeep b/tools/generators/.gitkeep similarity index 100% rename from tools/schematics/.gitkeep rename to tools/generators/.gitkeep diff --git a/tsconfig.base.json b/tsconfig.base.json index 8f77362e7..f569f8463 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,8 +27,14 @@ "@automapper/classes": [ "packages/classes/src/index.ts" ], - "@automapper/models": [ - "packages/models/src/index.ts" + "@automapper/types": [ + "packages/types/src/index.ts" + ], + "@automapper/integration-test": [ + "packages/integration-test/src/index.ts" + ], + "@automapper/pojos": [ + "packages/pojos/src/index.ts" ] } }, diff --git a/workspace.json b/workspace.json index bf456a0bd..977f1bc38 100644 --- a/workspace.json +++ b/workspace.json @@ -20,7 +20,10 @@ "options": { "jestConfig": "packages/core/jest.config.js", "passWithNoTests": true - } + }, + "outputs": [ + "coverage/packages/core" + ] }, "build": { "builder": "@nrwl/node:package", @@ -31,8 +34,12 @@ "main": "packages/core/src/index.ts", "assets": [ "packages/core/*.md" - ] - } + ], + "buildableProjectDepsInPackageJsonType": "peerDependencies" + }, + "outputs": [ + "{options.outputPath}" + ] } } }, @@ -55,7 +62,10 @@ "options": { "jestConfig": "packages/classes/jest.config.js", "passWithNoTests": true - } + }, + "outputs": [ + "coverage/packages/classes" + ] }, "build": { "builder": "@nrwl/node:package", @@ -66,14 +76,50 @@ "main": "packages/classes/src/index.ts", "assets": [ "packages/classes/*.md" + ], + "buildableProjectDepsInPackageJsonType": "peerDependencies" + }, + "outputs": [ + "{options.outputPath}" + ] + } + } + }, + "types": { + "root": "packages/types", + "sourceRoot": "packages/types/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "packages/types/**/*.ts" ] } + }, + "build": { + "builder": "@nrwl/node:package", + "options": { + "outputPath": "dist/packages/types", + "tsConfig": "packages/types/tsconfig.lib.json", + "packageJson": "packages/types/package.json", + "main": "packages/types/src/index.ts", + "assets": [ + "packages/types/*.md" + ], + "buildableProjectDepsInPackageJsonType": "peerDependencies" + }, + "outputs": [ + "{options.outputPath}" + ] } } }, - "models": { - "root": "packages/models", - "sourceRoot": "packages/models/src", + "integration-test": { + "root": "packages/integration-test", + "sourceRoot": "packages/integration-test/src", "projectType": "library", "schematics": {}, "architect": { @@ -81,20 +127,59 @@ "builder": "@nrwl/linter:eslint", "options": { "lintFilePatterns": [ - "packages/models/**/*.ts" + "packages/integration-test/**/*.ts" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "options": { + "jestConfig": "packages/integration-test/jest.config.js", + "passWithNoTests": true + }, + "outputs": [ + "coverage/packages/integration-test" + ] + } + } + }, + "pojos": { + "root": "packages/pojos", + "sourceRoot": "packages/pojos/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "packages/pojos/**/*.ts" ] } }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": [ + "coverage/packages/pojos" + ], + "options": { + "jestConfig": "packages/pojos/jest.config.js", + "passWithNoTests": true + } + }, "build": { "builder": "@nrwl/node:package", + "outputs": [ + "{options.outputPath}" + ], "options": { - "outputPath": "dist/packages/models", - "tsConfig": "packages/models/tsconfig.lib.json", - "packageJson": "packages/models/package.json", - "main": "packages/models/src/index.ts", + "outputPath": "dist/packages/pojos", + "tsConfig": "packages/pojos/tsconfig.lib.json", + "packageJson": "packages/pojos/package.json", + "main": "packages/pojos/src/index.ts", "assets": [ - "packages/models/*.md" - ] + "packages/pojos/*.md" + ], + "buildableProjectDepsInPackageJsonType": "peerDependencies" } } }