diff --git a/acceptance/repository-mongodb/src/__tests__/mongodb.datasource.ts b/acceptance/repository-mongodb/src/__tests__/mongodb.datasource.ts index 4a0866757122..84c5f233ea96 100644 --- a/acceptance/repository-mongodb/src/__tests__/mongodb.datasource.ts +++ b/acceptance/repository-mongodb/src/__tests__/mongodb.datasource.ts @@ -17,4 +17,5 @@ export const MONGODB_CONFIG: DataSourceOptions = { export const MONGODB_FEATURES: Partial = { idType: 'string', supportsTransactions: false, + convertIdType: true, }; diff --git a/packages/repository-tests/package-lock.json b/packages/repository-tests/package-lock.json index 3d4125b28dbd..039d4499fe62 100644 --- a/packages/repository-tests/package-lock.json +++ b/packages/repository-tests/package-lock.json @@ -9,12 +9,113 @@ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" }, + "@types/lodash": { + "version": "4.14.137", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.137.tgz", + "integrity": "sha512-g4rNK5SRKloO+sUGbuO7aPtwbwzMgjK+bm9BBhLD7jGUiGR7zhwYEhSln/ihgYQBeIJ5j7xjyaYzrWTcu3UotQ==", + "dev": true + }, "@types/node": { "version": "10.14.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz", "integrity": "sha512-CBR5avlLcu0YCILJiDIXeU2pTw7UK/NIxfC63m7d7CVamho1qDEzXKkOtEauQRPMy6MI8mLozth+JJkas7HY6g==", "dev": true }, + "accept-language": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", + "integrity": "sha1-9QJfF79lpGaoRYOMz5jNuHfYM4Q=", + "requires": { + "bcp47": "^1.1.2", + "stable": "^0.1.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcp47": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", + "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=" + }, + "bl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "cldrjs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.1.tgz", + "integrity": "sha512-xyiP8uAm8K1IhmpDndZLraloW1yqu0L+HYdQ7O1aGPxx9Cr+BMnPANlNhSt++UKfxytL2hd2NPXgTjiy7k43Ew==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -23,10 +124,454 @@ "ms": "^2.1.1" } }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "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" + } + }, + "globalize": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.4.2.tgz", + "integrity": "sha512-IfKeYI5mAITBmT5EnH8kSQB5uGson4Fkj2XtTpyEbIS7IHNfLHoeTyLJ6tfjiKC6cJXng3IhVurDk5C7ORqFhQ==", + "requires": { + "cldrjs": "^0.5.0" + } + }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "loopback-connector": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-4.8.0.tgz", + "integrity": "sha512-X5A4YaS6iifomym97ll/J/V+iPZ3Av69J6gmG3QXkq2JtzDtAOh6g2ITgo9aYJ/rzRf9HESH3Ys7aqeppDkpZQ==", + "requires": { + "async": "^2.1.5", + "bluebird": "^3.4.6", + "debug": "^3.1.0", + "msgpack5": "^4.2.0", + "strong-globalize": "^4.1.1", + "uuid": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "loopback-datasource-juggler": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-4.12.0.tgz", + "integrity": "sha512-22EpRS5jhUA1wnkmb8xvtvlFMt4x8X9ByBik6pBxc02Tgv28ZYb45D2eVp0X/wYdZ9Yw0mt3TgdexkfhYiYDoQ==", + "requires": { + "async": "^2.6.0", + "debug": "^4.1.0", + "depd": "^2.0.0", + "inflection": "^1.6.0", + "lodash": "^4.17.11", + "loopback-connector": "^4.4.0", + "minimatch": "^3.0.3", + "qs": "^6.5.0", + "shortid": "^2.2.6", + "strong-globalize": "^4.1.1", + "traverse": "^0.6.6", + "uuid": "^3.0.1" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "msgpack5": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.2.1.tgz", + "integrity": "sha512-Xo7nE9ZfBVonQi1rSopNAqPdts/QHyuSEUwIEzAkB+V2FtmkkLUbP6MyVqVVQxsZYI65FpvW3Bb8Z9ZWEjbgHQ==", + "requires": { + "bl": "^2.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.3.6", + "safe-buffer": "^5.1.2" + } + }, + "nanoid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.3.tgz", + "integrity": "sha512-NbaoqdhIYmY6FXDRB4eYtDVC9Z9eCbn8TyaiC16LNKtpPv/aqa0tOPD8y6gNE4yUNnaZ7LLhYtXOev/6+cBtfw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.8.0.tgz", + "integrity": "sha512-tPSkj8y92PfZVbinY1n84i1Qdx75lZjMQYx9WZhnkofyxzw2r7Ho39G3/aEvSUdebxpnnM4LZJCtvE/Aq3+s9w==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shortid": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz", + "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==", + "requires": { + "nanoid": "^2.0.0" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strong-globalize": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-4.1.3.tgz", + "integrity": "sha512-SJegV7w5D4AodEspZJtJ7rls3fmi+Zc0PdyJCqBsg4RN9B8TC80/uAI2fikC+s1Jp9FLvr2vDX8f0Fqc62M4OA==", + "requires": { + "accept-language": "^3.0.18", + "debug": "^4.1.1", + "globalize": "^1.4.2", + "lodash": "^4.17.4", + "md5": "^2.2.1", + "mkdirp": "^0.5.1", + "os-locale": "^3.1.0", + "yamljs": "^0.3.0" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + } } } } diff --git a/packages/repository-tests/package.json b/packages/repository-tests/package.json index 9c48b27c5e84..f7187160f703 100644 --- a/packages/repository-tests/package.json +++ b/packages/repository-tests/package.json @@ -23,15 +23,19 @@ "@loopback/build": "^1.7.1", "@loopback/repository": "^1.12.0", "@types/debug": "^4.1.5", - "@types/node": "^10.14.15" + "@types/node": "^10.14.15", + "@types/lodash": "^4.14.137" }, "dependencies": { "@loopback/testlab": "^1.7.4", + "loopback-datasource-juggler": "^4.12.0", + "lodash": "^4.17.15", "@types/debug": "^4.1.5", - "debug": "^4.1.1" + "debug": "^4.1.1", + "@loopback/context": "^1.21.3" }, "peerDependencies": { - "@loopback/repository": "^1.6.1" + "@loopback/repository": "^1.11.1" }, "files": [ "README.md", diff --git a/packages/repository-tests/src/crud-test-suite.ts b/packages/repository-tests/src/crud-test-suite.ts index 57ff976c234c..38a37ef0f4b6 100644 --- a/packages/repository-tests/src/crud-test-suite.ts +++ b/packages/repository-tests/src/crud-test-suite.ts @@ -33,6 +33,7 @@ export function crudRepositoryTestSuite( freeFormProperties: true, emptyValue: undefined, supportsTransactions: true, + convertIdType: false, ...partialFeatures, }; @@ -50,8 +51,10 @@ export function crudRepositoryTestSuite( }), ); + let testFiles = []; + const testRoot = path.resolve(__dirname, 'crud'); - let testFiles = fs.readdirSync(testRoot); + testFiles = fs.readdirSync(testRoot); testFiles = testFiles.filter(function(it) { return ( !!require.extensions[path.extname(it).toLowerCase()] && @@ -59,6 +62,29 @@ export function crudRepositoryTestSuite( ); }); + // relations folder tests + const folders = ['acceptance', 'integration']; + + for (const folder of folders) { + const relationsTestRoot = path.resolve( + __dirname, + `crud/relations/${folder}`, + ); + let folderTestFiles = fs.readdirSync(relationsTestRoot); + folderTestFiles = folderTestFiles.filter(function(it) { + return ( + (!!require.extensions[path.extname(it).toLowerCase()] && + /\.acceptance\.[^.]+$/.test(it)) || + (!!require.extensions[path.extname(it).toLowerCase()] && + /\.integration\.[^.]+$/.test(it)) + ); + }); + folderTestFiles.forEach(element => { + element = `relations/${folder}/` + element; + testFiles.push(element); + }); + } + for (const ix in testFiles) { const name = testFiles[ix]; const fullPath = path.resolve(testRoot, name); diff --git a/packages/repository-tests/src/crud/relations/acceptance/belongs-to.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/belongs-to.relation.acceptance.ts new file mode 100644 index 000000000000..bca7e21dc3e6 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/acceptance/belongs-to.relation.acceptance.ts @@ -0,0 +1,79 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../../types.repository-tests'; +import {Customer, Order, Shipment} from '../fixtures/models'; +import { + CustomerRepository, + OrderRepository, + ShipmentRepository, +} from '../fixtures/repositories'; +import {givenBoundCrudRepositories} from '../helpers'; + +export function belongsToRelationAcceptance( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + describe('BelongsTo relation (acceptance)', () => { + before(deleteAllModelsInDefaultDataSource); + + // Given a Customer and Order models - see definitions at the bottom + let customerRepo: CustomerRepository; + let orderRepo: OrderRepository; + let shipmentRepo: ShipmentRepository; + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + ({customerRepo, orderRepo, shipmentRepo} = givenBoundCrudRepositories( + ctx.dataSource, + )); + const models = [Customer, Order, Shipment]; + await ctx.dataSource.automigrate(models.map(m => m.name)); + }), + ); + + beforeEach(async () => { + await orderRepo.deleteAll(); + }); + + it('can find customer of order', async () => { + const customer = await customerRepo.create({ + name: 'Order McForder', + parentId: '1', + }); + const order = await orderRepo.create({ + customerId: customer.id, + description: 'Order from Order McForder, the hoarder of Mordor', + }); + + const result = await orderRepo.customer(order.id); + expect(result).to.deepEqual(customer); + }); + + it('can find shipment of order with a custom foreign key name', async () => { + const shipment = await shipmentRepo.create({ + name: 'Tuesday morning shipment', + }); + const order = await orderRepo.create({ + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: shipment.id, + description: 'Order that is shipped Tuesday morning', + }); + const result = await orderRepo.shipment(order.id); + expect(result).to.deepEqual(shipment); + }); + }); +} diff --git a/packages/repository-tests/src/crud/relations/acceptance/has-many-without-di.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/has-many-without-di.relation.acceptance.ts new file mode 100644 index 000000000000..911572855afd --- /dev/null +++ b/packages/repository-tests/src/crud/relations/acceptance/has-many-without-di.relation.acceptance.ts @@ -0,0 +1,205 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter} from '@loopback/context'; +import { + DefaultCrudRepository, + Entity, + EntityCrudRepository, + hasMany, + HasManyRepositoryFactory, + juggler, + model, + property, +} from '@loopback/repository'; +import {expect} from '@loopback/testlab'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../../types.repository-tests'; + +export function hasManyWithoutDIRelationAcceptance( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + describe('HasMany relation without di (acceptance)', () => { + before(deleteAllModelsInDefaultDataSource); + // Given a Customer and Order models - see definitions at the bottom + let existingCustomerId: string; + let ds: juggler.DataSource; + let customerRepo: CustomerRepository; + let orderRepo: OrderRepository; + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + ds = ctx.dataSource; + givenOrderRepository(); + givenCustomerRepository(); + await ctx.dataSource.automigrate([Customer.name, Order.name]); + }), + ); + + beforeEach(async () => { + await orderRepo.deleteAll(); + existingCustomerId = (await givenPersistedCustomerInstance()).id; + }); + + it('can create an instance of the related model', async () => { + async function createCustomerOrders( + customerId: string, + orderData: Partial, + ): Promise { + return customerRepo.orders(customerId).create(orderData); + } + const order = await createCustomerOrders(existingCustomerId, { + description: 'order 1', + }); + // avoid type problems of BSON type of mongodb + if (features.convertIdType) { + // eslint-disable-next-line require-atomic-updates + existingCustomerId = existingCustomerId.toString(); + } + expect(order.toObject()).containDeep({ + customerId: existingCustomerId, + description: 'order 1', + }); + + const persisted = await orderRepo.findById(order.id); + + expect(persisted.toObject()).to.deepEqual(order.toObject()); + }); + + it('can find instances of the related model (acceptance)', async () => { + async function createCustomerOrders( + customerId: string, + orderData: Partial, + ): Promise { + return customerRepo.orders(customerId).create(orderData); + } + async function findCustomerOrders(customerId: string) { + return customerRepo.orders(customerId).find(); + } + + const order = await createCustomerOrders(existingCustomerId, { + description: 'order 1', + }); + + const notMyOrder = await createCustomerOrders(existingCustomerId + 1, { + description: 'order 2', + }); + const orders = await findCustomerOrders(existingCustomerId); + + expect(orders).to.containEql(order); + expect(orders).to.not.containEql(notMyOrder); + + const persisted = await orderRepo.find({ + where: {customerId: existingCustomerId}, + }); + expect(persisted).to.deepEqual(orders); + }); + + //--- HELPERS ---// + + // use strictObjectIDCoercion here to make sure mongo's happy + @model({ + settings: { + strictObjectIDCoercion: true, + }, + }) + class Order extends Entity { + @property({ + type: features.idType, + id: true, + generated: true, + }) + id: string; + + @property({ + type: 'string', + required: true, + }) + description: string; + + @property({ + type: features.idType, + required: true, + }) + customerId: string; + } + + @model({ + settings: { + strictObjectIDCoercion: true, + }, + }) + class Customer extends Entity { + @property({ + type: features.idType, + id: true, + generated: true, + }) + id: string; + + @property({ + type: 'string', + }) + name: string; + + @hasMany(() => Order) + orders: Order[]; + } + + class OrderRepository extends DefaultCrudRepository< + Order, + typeof Order.prototype.id + > { + constructor(db: juggler.DataSource) { + super(Order, db); + } + } + + class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id + > { + public readonly orders: HasManyRepositoryFactory< + Order, + typeof Customer.prototype.id + >; + + constructor( + protected db: juggler.DataSource, + orderRepositoryGetter: Getter< + EntityCrudRepository + >, + ) { + super(Customer, db); + this.orders = this._createHasManyRepositoryFactoryFor( + 'orders', + orderRepositoryGetter, + ); + } + } + + function givenOrderRepository() { + orderRepo = new OrderRepository(ds); + } + + function givenCustomerRepository() { + customerRepo = new CustomerRepository(ds, Getter.fromValue(orderRepo)); + } + + async function givenPersistedCustomerInstance() { + return customerRepo.create({name: 'a customer'}); + } + }); +} diff --git a/packages/repository-tests/src/crud/relations/acceptance/has-many.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/has-many.relation.acceptance.ts new file mode 100644 index 000000000000..26441509b4af --- /dev/null +++ b/packages/repository-tests/src/crud/relations/acceptance/has-many.relation.acceptance.ts @@ -0,0 +1,249 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import * as _ from 'lodash'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../..'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; +import {Customer, Order} from '../fixtures/models'; +import {CustomerRepository, OrderRepository} from '../fixtures/repositories'; +import {givenBoundCrudRepositories} from '../helpers'; + +export function hasManyRelationAcceptance( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + describe('HasMany relation (acceptance)', () => { + before(deleteAllModelsInDefaultDataSource); + // Given a Customer and Order models - see definitions at the bottom + + let customerRepo: CustomerRepository; + let orderRepo: OrderRepository; + let existingCustomerId: string; + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + ({customerRepo, orderRepo} = givenBoundCrudRepositories( + ctx.dataSource, + )); + const models = [Customer, Order]; + await ctx.dataSource.automigrate(models.map(m => m.name)); + }), + ); + + beforeEach(async () => { + await customerRepo.deleteAll(); + await orderRepo.deleteAll(); + }); + + beforeEach(async () => { + existingCustomerId = (await givenPersistedCustomerInstance()).id; + // convert the type as it is generated as type number(in-memory, MySQL) or objectid(Mongo) + existingCustomerId = existingCustomerId.toString(); + }); + + it('can create an instance of the related model', async () => { + const order = await customerRepo.orders(existingCustomerId).create({ + description: 'order 1', + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: '1', + }); + + expect(order.toObject()).containDeep({ + customerId: existingCustomerId, + description: 'order 1', + }); + + const persisted = await orderRepo.findById(order.id); + expect(persisted.toObject()).to.deepEqual(order.toObject()); + }); + + it('can find instances of the related model', async () => { + const order = await createCustomerOrders(existingCustomerId, { + description: 'order 1', + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: '1', + }); + const notMyOrder = await createCustomerOrders(existingCustomerId + 1, { + description: 'order 2', + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: '1', + }); + const foundOrders = await findCustomerOrders(existingCustomerId); + expect(foundOrders).to.containEql(order); + expect(foundOrders).to.not.containEql(notMyOrder); + + const persisted = await orderRepo.find({ + where: {customerId: existingCustomerId}, + }); + expect(persisted).to.deepEqual(foundOrders); + }); + + it('can patch many instances', async () => { + await createCustomerOrders(existingCustomerId, { + description: 'order 1', + isShipped: false, + }); + await createCustomerOrders(existingCustomerId, { + description: 'order 2', + isShipped: false, + }); + const patchObject = {isShipped: true}; + const arePatched = await patchCustomerOrders( + existingCustomerId, + patchObject, + ); + expect(arePatched.count).to.equal(2); + const patchedData = _.map( + await findCustomerOrders(existingCustomerId), + d => _.pick(d, ['customerId', 'description', 'isShipped']), + ); + + expect(patchedData).to.eql([ + { + customerId: existingCustomerId, + description: 'order 1', + isShipped: true, + }, + { + customerId: existingCustomerId, + description: 'order 2', + isShipped: true, + }, + ]); + }); + + it('throws error when query tries to change the foreignKey', async () => { + await expect( + patchCustomerOrders(existingCustomerId, { + customerId: existingCustomerId + 1, + }), + ).to.be.rejectedWith(/Property "customerId" cannot be changed!/); + }); + + it('can delete many instances', async () => { + await createCustomerOrders(existingCustomerId, { + description: 'order 1', + }); + await createCustomerOrders(existingCustomerId, { + description: 'order 2', + }); + const deletedOrders = await deleteCustomerOrders(existingCustomerId); + expect(deletedOrders.count).to.equal(2); + const relatedOrders = await findCustomerOrders(existingCustomerId); + expect(relatedOrders).to.be.empty(); + }); + + it("does not delete instances that don't belong to the constrained instance", async () => { + const newOrder = { + customerId: existingCustomerId + 1, + description: 'another order', + }; + await orderRepo.create(newOrder); + await deleteCustomerOrders(existingCustomerId); + const orders = await orderRepo.find(); + expect(orders).to.have.length(1); + expect(_.pick(orders[0], ['customerId', 'description'])).to.eql(newOrder); + }); + + it('does not create an array of the related model', async () => { + await expect( + customerRepo.create({ + name: 'a customer', + orders: [ + { + description: 'order 1', + }, + ], + }), + ).to.be.rejectedWith(/`orders` is not defined/); + }); + + context('when targeting the source model', () => { + it('gets the parent entity through the child entity', async () => { + //Customer.definition.properties.id.type = String; + + const parent = await customerRepo.create({name: 'parent customer'}); + + const child = await customerRepo.create({ + name: 'child customer', + parentId: parent.id, + }); + const childsParent = await getParentCustomer(child.id); + expect(_.pick(childsParent, ['id', 'name'])).to.eql( + _.pick(parent, ['id', 'name']), + ); + }); + + it('creates a child entity through the parent entity', async () => { + const parent = await customerRepo.create({name: 'parent customer'}); + const child = await createCustomerChildren(parent.id, { + name: 'child customer', + }); + // in-memory, MySQL generat ids as numbers, and MongoDB generates it as ObjectId + parent.id = parent.id.toString(); + + expect(child.parentId).to.equal(parent.id); + + const children = await findCustomerChildren(parent.id); + expect(children).to.containEql(child); + }); + }); + + // This should be enforced by the database to avoid race conditions + it.skip('reject create request when the customer does not exist'); + + // repository helper methods + async function createCustomerOrders( + customerId: string, + orderData: Partial, + ): Promise { + return customerRepo.orders(customerId).create(orderData); + } + + async function findCustomerOrders(customerId: string) { + return customerRepo.orders(customerId).find(); + } + + async function patchCustomerOrders( + customerId: string, + order: Partial, + ) { + return customerRepo.orders(customerId).patch(order); + } + + async function deleteCustomerOrders(customerId: string) { + return customerRepo.orders(customerId).delete(); + } + + async function getParentCustomer(customerId: string) { + return customerRepo.parent(customerId); + } + + async function createCustomerChildren( + customerId: string, + customerData: Partial, + ) { + return customerRepo.customers(customerId).create(customerData); + } + + async function findCustomerChildren(customerId: string) { + return customerRepo.customers(customerId).find(); + } + + async function givenPersistedCustomerInstance() { + return customerRepo.create({name: 'a customer'}); + } + }); +} diff --git a/packages/repository-tests/src/crud/relations/acceptance/has-one.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/has-one.relation.acceptance.ts new file mode 100644 index 000000000000..5c1238cd62e4 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/acceptance/has-one.relation.acceptance.ts @@ -0,0 +1,253 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {EntityNotFoundError, Filter} from '@loopback/repository'; +import {expect, toJSON} from '@loopback/testlab'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../..'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; +import {Address, Customer} from '../fixtures/models'; +import {AddressRepository, CustomerRepository} from '../fixtures/repositories'; +import {givenBoundCrudRepositories} from '../helpers'; + +export function hasOneRelationAcceptance( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + describe('hasOne relation (acceptance)', () => { + let customerRepo: CustomerRepository; + let addressRepo: AddressRepository; + let existingCustomerId: string; + + before(deleteAllModelsInDefaultDataSource); + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + ({customerRepo, addressRepo} = givenBoundCrudRepositories( + ctx.dataSource, + )); + const models = [Customer, Address]; + await ctx.dataSource.automigrate(models.map(m => m.name)); + }), + ); + + beforeEach(async () => { + await addressRepo.deleteAll(); + existingCustomerId = (await givenPersistedCustomerInstance()).id; + // convert the type as it is generated as type number(in-memory, MySQL) or objectid(Mongo) + existingCustomerId = existingCustomerId.toString(); + }); + + it('can create an instance of the related model', async () => { + const address = await createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + expect(address.toObject()).to.containDeep({ + customerId: existingCustomerId, + street: '123 test avenue', + }); + + expect(address.customerId).eql(existingCustomerId); + + const persisted = await addressRepo.findById(address.id); + expect(persisted.toObject()).to.deepEqual(address.toObject()); + }); + + // We do not enforce referential integrity at the moment. It is up to + // our users to set up unique constraint(s) between related models at the + // database level + it.skip('refuses to create related model instance twice', async () => { + const address = await createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + await expect( + createCustomerAddress(existingCustomerId, { + street: '456 test street', + }), + ).to.be.rejectedWith(/Duplicate entry for Address.customerId/); + expect(address.toObject()).to.containDeep({ + customerId: existingCustomerId, + street: '123 test avenue', + }); + + const persisted = await addressRepo.findById(address.id); + expect(persisted.toObject()).to.deepEqual(address.toObject()); + }); + + it('can find instance of the related model', async () => { + const address = await createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + const foundAddress = await findCustomerAddress(existingCustomerId); + expect(foundAddress).to.containEql(address); + expect(toJSON(foundAddress)).to.deepEqual(toJSON(address)); + + const persisted = await addressRepo.find({ + where: {customerId: existingCustomerId}, + }); + expect(persisted[0]).to.deepEqual(foundAddress); + }); + + // FIXME(b-admike): make sure the test fails with compiler error + it.skip('ignores where filter to find related model instance', async () => { + const foundAddress = await findCustomerAddressWithFilter( + existingCustomerId, + // the compiler should complain that the where field is + // not accepted in the filter object for the get() method + // if the following line is uncommented + { + where: {street: '456 test road'}, + }, + ); + + const persisted = await addressRepo.find({ + where: {customerId: existingCustomerId}, + }); + // TODO: make sure this test fails when where condition is supplied + // compiler should have errored out (?) + expect(persisted[0]).to.deepEqual(foundAddress); + }); + + it('reports EntityNotFound error when related model is deleted', async () => { + const address = await createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + await addressRepo.deleteById(address.id); + + await expect(findCustomerAddress(existingCustomerId)).to.be.rejectedWith( + EntityNotFoundError, + ); + }); + + it('can PATCH hasOne instances', async () => { + const address = await createCustomerAddress(existingCustomerId, { + street: '1 Amedee Bonnet', + zipcode: '69740', + city: 'Genas', + province: 'Rhone', + }); + + const patchObject = {city: 'Lyon-Genas'}; + const arePatched = await patchCustomerAddress( + existingCustomerId, + patchObject, + ); + + expect(arePatched).to.deepEqual({count: 1}); + const patchedData = await addressRepo.findById(address.id); + // make mongo happy + if (features.convertIdType) { + address.id = address.id.toString(); + } + + expect(toJSON(patchedData)).to.deepEqual({ + id: address.id, + customerId: existingCustomerId, + street: '1 Amedee Bonnet', + zipcode: '69740', + city: 'Lyon-Genas', + province: 'Rhone', + }); + }); + + it('patches the related instance only', async () => { + const bob = await customerRepo.create({name: 'Bob'}); + await customerRepo.address(bob.id).create({city: 'Paris'}); + + const alice = await customerRepo.create({name: 'Alice'}); + await customerRepo.address(alice.id).create({city: 'London'}); + + const result = await patchCustomerAddress(alice.id, { + city: 'New York', + }); + + expect(result).to.deepEqual({count: 1}); + + const found = await customerRepo.address(bob.id).get(); + expect(toJSON(found)).to.containDeep({city: 'Paris'}); + }); + + it('throws an error when PATCH tries to change the foreignKey', async () => { + await expect( + patchCustomerAddress(existingCustomerId, { + customerId: existingCustomerId + 1, + }), + ).to.be.rejectedWith(/Property "customerId" cannot be changed!/); + }); + + it('can DELETE hasOne relation instances', async () => { + await createCustomerAddress(existingCustomerId, { + street: '1 Amedee Bonnet', + zipcode: '69740', + city: 'Genas', + province: 'Rhone', + }); + + const areDeleted = await deleteCustomerAddress(existingCustomerId); + expect(areDeleted).to.deepEqual({count: 1}); + + await expect(findCustomerAddress(existingCustomerId)).to.be.rejectedWith( + EntityNotFoundError, + ); + }); + + it('deletes the related model instance only', async () => { + const bob = await customerRepo.create({name: 'Bob'}); + await customerRepo.address(bob.id).create({city: 'Paris'}); + + const alice = await customerRepo.create({name: 'Alice'}); + await customerRepo.address(alice.id).create({city: 'London'}); + + const result = await deleteCustomerAddress(alice.id); + + expect(result).to.deepEqual({count: 1}); + + const found = await addressRepo.find(); + expect(found).to.have.length(1); + }); + + /*---------------- HELPERS -----------------*/ + + async function createCustomerAddress( + customerId: string, + addressData: Partial
, + ): Promise
{ + return customerRepo.address(customerId).create(addressData); + } + + async function findCustomerAddress(customerId: string) { + return customerRepo.address(customerId).get(); + } + + async function findCustomerAddressWithFilter( + customerId: string, + filter: Filter
, + ) { + return customerRepo.address(customerId).get(filter); + } + async function patchCustomerAddress( + customerId: string, + addressData: Partial
, + ) { + return customerRepo.address(customerId).patch(addressData); + } + + async function deleteCustomerAddress(customerId: string) { + return customerRepo.address(customerId).delete(); + } + + async function givenPersistedCustomerInstance() { + return customerRepo.create({name: 'a customer'}); + } + }); +} diff --git a/packages/repository-tests/src/crud/relations/acceptance/repository.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/repository.acceptance.ts new file mode 100644 index 000000000000..16f125be3387 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/acceptance/repository.acceptance.ts @@ -0,0 +1,167 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + AnyObject, + DefaultCrudRepository, + Entity, + model, + property, +} from '@loopback/repository'; +import {juggler} from '@loopback/repository/src'; +import {expect, skipIf} from '@loopback/testlab'; +import {Suite} from 'mocha'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../..'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; +import {Product} from '../fixtures/models'; +import {ProductRepository} from '../fixtures/repositories'; + +export function hasOneRelationAcceptance( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + // This test shows the recommended way how to use @loopback/repository + // together with existing connectors when building LoopBack applications + describe('Repository in Thinking in LoopBack', () => { + let repo: ProductRepository; + let db: juggler.DataSource; + + before(deleteAllModelsInDefaultDataSource); + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + db = ctx.dataSource; + Product.definition.properties.id.type = features.idType; + givenProductRepository(); + await ctx.dataSource.automigrate(Product.name); + }), + ); + + beforeEach(() => repo.deleteAll()); + + it('counts models in empty database', async () => { + expect(await repo.count()).to.deepEqual({count: 0}); + }); + + it('creates a new model', async () => { + const p: Product = await repo.create({name: 'Ink Pen', slug: 'pen'}); + expect(p).instanceof(Product); + expect.exists(p.id); + }); + + it('can save a model', async () => { + const p = await repo.create({slug: 'pencil'}); + + p.name = 'Red Pencil'; + await repo.save(p); + + await repo.findById(p.id); + expect(p).to.have.properties({ + slug: 'pencil', + name: 'Red Pencil', + }); + }); + + it('rejects extra model properties (defaults to strict mode)', async () => { + await expect( + repo.create({ + name: 'custom', + extra: 'additional-data', + } as AnyObject), + ).to.be.rejectedWith(/extra.*not defined/); + }); + // skip this test for MySQL + skipIf<[(this: Suite) => void], void>( + !features.freeFormProperties, + describe, + 'allows models to allow additional properties', + () => { + it('allows models to allow additional properties', async () => { + // TODO(bajtos) Add syntactic sugar to allow the following usage: + // @model({strict: false}) + @model({settings: {strict: false}}) + class Flexible extends Entity { + @property({id: true}) + id: number; + } + const flexibleRepo = new DefaultCrudRepository< + Flexible, + typeof Flexible.prototype.id + >(Flexible, db); + + const created = await flexibleRepo.create({ + extra: 'additional data', + } as AnyObject); + const stored = await flexibleRepo.findById(created.id); + expect(stored).to.containDeep({extra: 'additional data'}); + }); + }, + ); + + it('allows models to allow nested model properties', async () => { + @model() + class Role extends Entity { + @property() + name: string; + } + + @model() + class Address extends Entity { + @property() + street: string; + } + + @model() + class User extends Entity { + @property({ + type: 'number', + id: true, + generated: true, + }) + id: number; + + @property({type: 'string'}) + name: string; + + @property.array(Role) + roles: Role[]; + + @property() + address: Address; + } + + const userRepo = new DefaultCrudRepository< + User, + typeof User.prototype.id + >(User, db); + + const models = [User, Role, Address]; + await db.automigrate(models.map(m => m.name)); + + const user = { + name: 'foo', + roles: [{name: 'admin'}, {name: 'user'}], + address: {street: 'backstreet'}, + }; + const created = await userRepo.create(user); + + const stored = await userRepo.findById(created.id); + expect(stored).to.containDeep(user); + }); + + function givenProductRepository() { + repo = new ProductRepository(db); + } + }); +} diff --git a/packages/repository/src/__tests__/fixtures/models/address.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/address.model.ts similarity index 66% rename from packages/repository/src/__tests__/fixtures/models/address.model.ts rename to packages/repository-tests/src/crud/relations/fixtures/models/address.model.ts index c886c6d76214..af3273cc7287 100644 --- a/packages/repository/src/__tests__/fixtures/models/address.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/address.model.ts @@ -1,33 +1,45 @@ // Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository +// Node module: @loopback/repository-tests // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {belongsTo, Entity, model, property} from '../../..'; +import {belongsTo, Entity, model, property} from '@loopback/repository'; import {Customer, CustomerWithRelations} from './customer.model'; -@model() +@model({ + settings: { + strictObjectIDCoercion: true, + }, +}) export class Address extends Entity { + @property({ + type: 'string', + id: true, + generated: true, + }) + id: string; @property({ type: 'string', }) street: string; @property({ type: 'string', - id: true, + default: '12345', }) zipcode: string; @property({ type: 'string', + default: 'Toronto', }) city: string; @property({ type: 'string', + default: 'Ontario', }) province: string; @belongsTo(() => Customer) - customerId: number; + customerId: string; } export interface AddressRelations { diff --git a/packages/repository/src/__tests__/fixtures/models/customer.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts similarity index 74% rename from packages/repository/src/__tests__/fixtures/models/customer.model.ts rename to packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts index fd18dc9e82dd..086e3f146abd 100644 --- a/packages/repository/src/__tests__/fixtures/models/customer.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts @@ -1,19 +1,31 @@ // Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository +// Node module: @loopback/repository-tests // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {belongsTo, Entity, hasMany, hasOne, model, property} from '../../..'; +import { + belongsTo, + Entity, + hasMany, + hasOne, + model, + property, +} from '@loopback/repository'; import {Address, AddressWithRelations} from './address.model'; import {Order, OrderWithRelations} from './order.model'; -@model() +@model({ + settings: { + strictObjectIDCoercion: true, + }, +}) export class Customer extends Entity { @property({ - type: 'number', + type: 'string', id: true, + generated: true, }) - id: number; + id: string; @property({ type: 'string', @@ -30,7 +42,7 @@ export class Customer extends Entity { customers?: Customer[]; @belongsTo(() => Customer) - parentId?: number; + parentId?: string; } export interface CustomerRelations { diff --git a/packages/repository/src/__tests__/fixtures/models/index.ts b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts similarity index 88% rename from packages/repository/src/__tests__/fixtures/models/index.ts rename to packages/repository-tests/src/crud/relations/fixtures/models/index.ts index 9c1ee60840f0..af0499873a13 100644 --- a/packages/repository/src/__tests__/fixtures/models/index.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts @@ -1,5 +1,5 @@ // Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository +// Node module: @loopback/repository-tests // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT diff --git a/packages/repository/src/__tests__/fixtures/models/order.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts similarity index 75% rename from packages/repository/src/__tests__/fixtures/models/order.model.ts rename to packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts index f4256925d89a..b12de55ac104 100644 --- a/packages/repository/src/__tests__/fixtures/models/order.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts @@ -1,17 +1,22 @@ // Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository +// Node module: @loopback/repository-tests // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {belongsTo, Entity, model, property} from '../../..'; +import {belongsTo, Entity, model, property} from '@loopback/repository'; import {Customer, CustomerWithRelations} from './customer.model'; import {Shipment, ShipmentWithRelations} from './shipment.model'; -@model() +@model({ + settings: { + strictObjectIDCoercion: true, + }, +}) export class Order extends Entity { @property({ type: 'string', id: true, + generated: true, }) id: string; @@ -24,14 +29,15 @@ export class Order extends Entity { @property({ type: 'boolean', required: false, + default: false, }) isShipped: boolean; @belongsTo(() => Customer) - customerId: number; + customerId: string; @belongsTo(() => Shipment, {name: 'shipment'}) - shipment_id: number; + shipment_id: string; } export interface OrderRelations { diff --git a/packages/repository/src/__tests__/fixtures/models/product.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/product.model.ts similarity index 72% rename from packages/repository/src/__tests__/fixtures/models/product.model.ts rename to packages/repository-tests/src/crud/relations/fixtures/models/product.model.ts index 7612e577e8e1..6b294c8cd3d1 100644 --- a/packages/repository/src/__tests__/fixtures/models/product.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/product.model.ts @@ -1,18 +1,23 @@ // Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository +// Node module: @loopback/repository-tests // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Entity, model, property} from '../../..'; +import {Entity, model, property} from '@loopback/repository'; -@model() +@model({ + settings: { + strictObjectIDCoercion: true, + }, +}) export class Product extends Entity { @property({ - type: 'number', + type: 'string', id: true, + generated: true, description: 'The unique identifier for a product', }) - id: number; + id: string; @property({type: 'string'}) name: string; diff --git a/packages/repository/src/__tests__/fixtures/models/shipment.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/shipment.model.ts similarity index 72% rename from packages/repository/src/__tests__/fixtures/models/shipment.model.ts rename to packages/repository-tests/src/crud/relations/fixtures/models/shipment.model.ts index cf9787af8448..5fc7cdd8e70a 100644 --- a/packages/repository/src/__tests__/fixtures/models/shipment.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/shipment.model.ts @@ -1,18 +1,23 @@ // Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository +// Node module: @loopback/repository-tests // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Entity, hasMany, model, property} from '../../..'; +import {Entity, hasMany, model, property} from '@loopback/repository'; import {Order, OrderWithRelations} from './order.model'; -@model() +@model({ + settings: { + strictObjectIDCoercion: true, + }, +}) export class Shipment extends Entity { @property({ - type: 'number', + type: 'string', id: true, + generated: true, }) - id: number; + id: string; @property({type: 'string'}) name: string; diff --git a/packages/repository/src/__tests__/fixtures/repositories/address.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/address.repository.ts similarity index 94% rename from packages/repository/src/__tests__/fixtures/repositories/address.repository.ts rename to packages/repository-tests/src/crud/relations/fixtures/repositories/address.repository.ts index 3be973cef13c..59230dcbb119 100644 --- a/packages/repository/src/__tests__/fixtures/repositories/address.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/address.repository.ts @@ -9,13 +9,13 @@ import { DefaultCrudRepository, juggler, repository, -} from '../../..'; +} from '@loopback/repository'; import {Address, AddressRelations, Customer} from '../models'; import {CustomerRepository} from '../repositories'; export class AddressRepository extends DefaultCrudRepository< Address, - typeof Address.prototype.zipcode, + typeof Address.prototype.id, AddressRelations > { public readonly customer: BelongsToAccessor< diff --git a/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts similarity index 96% rename from packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts rename to packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts index 9a2dcb302c98..31b3a9ecc562 100644 --- a/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts @@ -8,10 +8,10 @@ import { BelongsToAccessor, DefaultCrudRepository, HasManyRepositoryFactory, + HasOneRepositoryFactory, juggler, repository, -} from '../../..'; -import {HasOneRepositoryFactory} from '../../../'; +} from '@loopback/repository'; import {Address, Customer, CustomerRelations, Order} from '../models'; import {AddressRepository} from './address.repository'; import {OrderRepository} from './order.repository'; @@ -54,6 +54,7 @@ export class CustomerRepository extends DefaultCrudRepository< 'address', addressRepositoryGetter, ); + this.customers = this.createHasManyRepositoryFactoryFor( 'customers', Getter.fromValue(this), diff --git a/packages/repository/src/__tests__/fixtures/repositories/index.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts similarity index 100% rename from packages/repository/src/__tests__/fixtures/repositories/index.ts rename to packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts diff --git a/packages/repository/src/__tests__/fixtures/repositories/order.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/order.repository.ts similarity index 97% rename from packages/repository/src/__tests__/fixtures/repositories/order.repository.ts rename to packages/repository-tests/src/crud/relations/fixtures/repositories/order.repository.ts index bd7bf0a7b32f..429e3681bc15 100644 --- a/packages/repository/src/__tests__/fixtures/repositories/order.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/order.repository.ts @@ -9,7 +9,7 @@ import { DefaultCrudRepository, juggler, repository, -} from '../../..'; +} from '@loopback/repository'; import {Customer, Order, OrderRelations, Shipment} from '../models'; import {CustomerRepository, ShipmentRepository} from '../repositories'; @@ -39,6 +39,7 @@ export class OrderRepository extends DefaultCrudRepository< 'customer', customerRepositoryGetter, ); + this.shipment = this.createBelongsToAccessorFor( 'shipment', shipmentRepositoryGetter, diff --git a/packages/repository/src/__tests__/fixtures/repositories/product.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/product.repository.ts similarity index 86% rename from packages/repository/src/__tests__/fixtures/repositories/product.repository.ts rename to packages/repository-tests/src/crud/relations/fixtures/repositories/product.repository.ts index 2ab4b0408e7d..d8de7a8a2143 100644 --- a/packages/repository/src/__tests__/fixtures/repositories/product.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/product.repository.ts @@ -3,9 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {DefaultCrudRepository, juggler} from '../../..'; +import {DefaultCrudRepository, juggler} from '@loopback/repository'; import {Product, ProductRelations} from '../models/product.model'; -export {Product}; export class ProductRepository extends DefaultCrudRepository< Product, diff --git a/packages/repository/src/__tests__/fixtures/repositories/shipment.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/shipment.repository.ts similarity index 92% rename from packages/repository/src/__tests__/fixtures/repositories/shipment.repository.ts rename to packages/repository-tests/src/crud/relations/fixtures/repositories/shipment.repository.ts index 9b7aea3b768a..50b5868ae1d4 100644 --- a/packages/repository/src/__tests__/fixtures/repositories/shipment.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/shipment.repository.ts @@ -4,14 +4,14 @@ // License text available at https://opensource.org/licenses/MIT import {Getter, inject} from '@loopback/context'; -import {OrderRepository} from '.'; import { DefaultCrudRepository, HasManyRepositoryFactory, juggler, repository, -} from '../../..'; +} from '@loopback/repository'; import {Order, Shipment, ShipmentRelations} from '../models'; +import {OrderRepository} from '../repositories'; export class ShipmentRepository extends DefaultCrudRepository< Shipment, diff --git a/packages/repository-tests/src/crud/relations/helpers.ts b/packages/repository-tests/src/crud/relations/helpers.ts new file mode 100644 index 000000000000..066b78ca71fc --- /dev/null +++ b/packages/repository-tests/src/crud/relations/helpers.ts @@ -0,0 +1,30 @@ +import {juggler} from '@loopback/repository'; +import { + AddressRepository, + CustomerRepository, + OrderRepository, + ShipmentRepository, +} from './fixtures/repositories'; + +export function givenBoundCrudRepositories(db: juggler.DataSource) { + const customerRepo: CustomerRepository = new CustomerRepository( + db, + async () => orderRepo, + async () => addressRepo, + ); + const orderRepo: OrderRepository = new OrderRepository( + db, + async () => customerRepo, + async () => shipmentRepo, + ); + const shipmentRepo: ShipmentRepository = new ShipmentRepository( + db, + async () => orderRepo, + ); + const addressRepo: AddressRepository = new AddressRepository( + db, + async () => customerRepo, + ); + + return {customerRepo, orderRepo, shipmentRepo, addressRepo}; +} diff --git a/packages/repository-tests/src/crud/relations/integration/belongs-to.factory.integration.ts b/packages/repository-tests/src/crud/relations/integration/belongs-to.factory.integration.ts new file mode 100644 index 000000000000..6302bdf484d2 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/integration/belongs-to.factory.integration.ts @@ -0,0 +1,146 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + BelongsToAccessor, + BelongsToDefinition, + createBelongsToAccessor, + Entity, + EntityCrudRepository, + EntityNotFoundError, + Getter, + juggler, + ModelDefinition, + RelationType, +} from '@loopback/repository'; +import {expect} from '@loopback/testlab'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../..'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; + +export function belongsToFactorySuite( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + // Given a Customer and Order models - see definitions at the bottom + let db: juggler.DataSource; + let customerRepo: EntityCrudRepository< + Customer, + typeof Customer.prototype.id + >; + let orderRepo: EntityCrudRepository; + + describe('BelongsTo relation (integration)', () => { + let findCustomerOfOrder: BelongsToAccessor< + Customer, + typeof Order.prototype.id + >; + + before(deleteAllModelsInDefaultDataSource); + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + db = ctx.dataSource; + givenCrudRepositories(); + await ctx.dataSource.automigrate(Customer.name); + await ctx.dataSource.automigrate(Order.name); + }), + ); + + before(givenAccessor); + beforeEach(async function resetDatabase() { + await Promise.all([customerRepo.deleteAll(), orderRepo.deleteAll()]); + }); + + it('finds an instance of the related model', async () => { + const customer = await customerRepo.create({ + name: 'Order McForder', + orders: [], + }); + const order = await orderRepo.create({ + customerId: customer.id, + description: 'Order from Order McForder, the hoarder of Mordor', + }); + + const result = await findCustomerOfOrder(order.id); + + expect(result).to.deepEqual(customer); + }); + + it('throws EntityNotFound error when the related model does not exist', async () => { + const order = await orderRepo.create({ + customerId: 999, // does not exist + description: 'Order of a fictional customer', + }); + + await expect(findCustomerOfOrder(order.id)).to.be.rejectedWith( + EntityNotFoundError, + ); + }); + + //--- HELPERS ---// + + function givenAccessor() { + findCustomerOfOrder = createBelongsToAccessor( + Order.definition.relations.customer as BelongsToDefinition, + Getter.fromValue(customerRepo), + orderRepo, + ); + } + }); + + //--- HELPERS ---// + + class Order extends Entity { + id: number; + description: string; + customerId: number; + + static definition = new ModelDefinition('Order') + .addProperty('id', {type: features.idType, id: true, generated: true}) + .addProperty('description', {type: 'string', required: true}) + .addProperty('customerId', {type: features.idType, required: true}) + .addRelation({ + name: 'customer', + type: RelationType.belongsTo, + source: Order, + target: () => Customer, + keyFrom: 'customerId', + keyTo: 'id', + }); + } + + class Customer extends Entity { + id: number; + name: string; + orders: Order[]; + + static definition: ModelDefinition = new ModelDefinition('Customer') + .addProperty('id', {type: features.idType, id: true, generated: true}) + .addProperty('name', {type: 'string', required: true}) + .addProperty('orders', {type: Order, array: true}) + .addRelation({ + name: 'orders', + type: RelationType.hasMany, + targetsMany: true, + source: Customer, + target: () => Order, + keyTo: 'customerId', + }); + } + + function givenCrudRepositories() { + customerRepo = new repositoryClass(Customer, db); + orderRepo = new repositoryClass(Order, db); + } +} diff --git a/packages/repository-tests/src/crud/relations/integration/has-many.factory.integration.ts b/packages/repository-tests/src/crud/relations/integration/has-many.factory.integration.ts new file mode 100644 index 000000000000..4a3149862b28 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/integration/has-many.factory.integration.ts @@ -0,0 +1,268 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + createHasManyRepositoryFactory, + Entity, + EntityCrudRepository, + Getter, + HasManyDefinition, + HasManyRepository, + HasManyRepositoryFactory, + juggler, + ModelDefinition, + RelationType, +} from '@loopback/repository'; +import {expect} from '@loopback/testlab'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../..'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; + +export function hasManyFactorySuite( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + // Given a Customer and Order models - see definitions at the bottom + let db: juggler.DataSource; + let customerRepo: EntityCrudRepository< + Customer, + typeof Customer.prototype.id + >; + let orderRepo: EntityCrudRepository; + let reviewRepo: EntityCrudRepository; + + describe('HasMany relation (integration)', () => { + let existingCustomerId: string; + + let customerOrderRepo: HasManyRepository; + let customerAuthoredReviewFactoryFn: HasManyRepositoryFactory< + Review, + typeof Customer.prototype.id + >; + let customerApprovedReviewFactoryFn: HasManyRepositoryFactory< + Review, + typeof Customer.prototype.id + >; + + before(deleteAllModelsInDefaultDataSource); + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + db = ctx.dataSource; + givenCrudRepositories(); + await givenPersistedCustomerInstance(); + givenConstrainedRepositories(); + givenRepositoryFactoryFunctions(); + await ctx.dataSource.automigrate(Review.name); + await ctx.dataSource.automigrate(Customer.name); + await ctx.dataSource.automigrate(Order.name); + }), + ); + + beforeEach(async function resetDatabase() { + await orderRepo.deleteAll(); + await reviewRepo.deleteAll(); + }); + + it('can create an instance of the related model', async () => { + const order = await customerOrderRepo.create({ + description: 'an order desc', + customerId: existingCustomerId, + }); + const persisted = await orderRepo.findById(order.id); + // make mongo happy + if (features.convertIdType) { + persisted.customerId = persisted.customerId.toString(); + } + expect(order).to.deepEqual(persisted); + }); + + it('can find an instance of the related model', async () => { + const order = await customerOrderRepo.create({ + description: 'an order desc', + customerId: existingCustomerId, + }); + const notMyOrder = await orderRepo.create({ + description: "someone else's order desc", + customerId: existingCustomerId + 1, // a different customerId, + }); + + const persistedOrders = await orderRepo.find({ + where: { + customerId: existingCustomerId, + }, + }); + + const orders = await customerOrderRepo.find(); + expect(orders).to.deepEqual(persistedOrders); + // make mongo happy + if (features.convertIdType) { + for (const o of orders) { + o.customerId = o.customerId.toString(); + } + } + expect(orders).to.containEql(order); + expect(orders).to.not.containEql(notMyOrder); + }); + + it('finds appropriate related model instances for multiple relations', async () => { + // note(shimks): roundabout way of creating reviews with 'approves' + // ideally, the review repository should have a approve function + // which should 'approve' a review + // On another note, this test should be separated for 'create' and 'find' + await customerAuthoredReviewFactoryFn(existingCustomerId).create({ + description: 'my wonderful review', + approvedId: existingCustomerId + 1, + }); + await customerAuthoredReviewFactoryFn(existingCustomerId + 1).create({ + description: 'smash that progenitor loving approve button', + approvedId: existingCustomerId, + }); + + const reviewsApprovedByCustomerOne = await customerApprovedReviewFactoryFn( + existingCustomerId, + ).find(); + const reviewsApprovedByCustomerTwo = await customerApprovedReviewFactoryFn( + existingCustomerId + 1, + ).find(); + + const persistedReviewsApprovedByCustomerOne = await reviewRepo.find({ + where: { + approvedId: existingCustomerId, + }, + }); + const persistedReviewsApprovedByCustomerTwo = await reviewRepo.find({ + where: { + approvedId: existingCustomerId + 1, + }, + }); + + expect(reviewsApprovedByCustomerOne).to.eql( + persistedReviewsApprovedByCustomerOne, + ); + expect(reviewsApprovedByCustomerTwo).to.eql( + persistedReviewsApprovedByCustomerTwo, + ); + }); + + //--- HELPERS ---// + + async function givenPersistedCustomerInstance() { + const customer = await customerRepo.create({name: 'a customer'}); + existingCustomerId = customer.id; + } + + function givenConstrainedRepositories() { + const orderFactoryFn = createHasManyRepositoryFactory< + Order, + typeof Order.prototype.id, + typeof Customer.prototype.id + >( + Customer.definition.relations.orders as HasManyDefinition, + Getter.fromValue(orderRepo), + ); + + customerOrderRepo = orderFactoryFn(existingCustomerId); + } + + function givenRepositoryFactoryFunctions() { + customerAuthoredReviewFactoryFn = createHasManyRepositoryFactory( + Customer.definition.relations.reviewsAuthored as HasManyDefinition, + Getter.fromValue(reviewRepo), + ); + customerApprovedReviewFactoryFn = createHasManyRepositoryFactory( + Customer.definition.relations.reviewsApproved as HasManyDefinition, + Getter.fromValue(reviewRepo), + ); + } + }); + + function givenCrudRepositories() { + customerRepo = new repositoryClass(Customer, db); + orderRepo = new repositoryClass(Order, db); + reviewRepo = new repositoryClass(Review, db); + } + + //--- HELPERS ---// + + class Order extends Entity { + id: string; + description: string; + customerId: string; + + static definition = new ModelDefinition('Order') + .addProperty('id', {type: features.idType, id: true, generated: true}) + .addProperty('description', {type: 'string', required: true}) + .addProperty('customerId', {type: 'string'}) + .addRelation({ + name: 'customer', + type: RelationType.belongsTo, + source: Order, + target: () => Customer, + keyFrom: 'customerId', + keyTo: 'id', + }); + } + + class Review extends Entity { + id: string; + description: string; + authorId: string; + approvedId: string; + + static definition = new ModelDefinition('Review') + .addProperty('id', {type: features.idType, id: true, generated: true}) + .addProperty('description', {type: 'string', required: true}) + .addProperty('authorId', {type: features.idType, required: false}) + .addProperty('approvedId', {type: features.idType, required: false}); + } + + class Customer extends Entity { + id: string; + name: string; + orders: Order[]; + reviewsAuthored: Review[]; + reviewsApproved: Review[]; + + static definition: ModelDefinition = new ModelDefinition('Customer') + .addProperty('id', {type: features.idType, id: true, generated: true}) + .addProperty('name', {type: 'string', required: true}) + .addProperty('orders', {type: Order, array: true}) + .addProperty('reviewsAuthored', {type: Review, array: true}) + .addProperty('reviewsApproved', {type: Review, array: true}) + .addRelation({ + name: 'orders', + type: RelationType.hasMany, + targetsMany: true, + source: Customer, + target: () => Order, + keyTo: 'customerId', + }) + .addRelation({ + name: 'reviewsAuthored', + type: RelationType.hasMany, + targetsMany: true, + source: Customer, + target: () => Review, + keyTo: 'authorId', + }) + .addRelation({ + name: 'reviewsApproved', + type: RelationType.hasMany, + targetsMany: true, + source: Customer, + target: () => Review, + keyTo: 'approvedId', + }); + } +} diff --git a/packages/repository-tests/src/types.repository-tests.ts b/packages/repository-tests/src/types.repository-tests.ts index 67c59aec15de..a7d99acb9393 100644 --- a/packages/repository-tests/src/types.repository-tests.ts +++ b/packages/repository-tests/src/types.repository-tests.ts @@ -47,6 +47,7 @@ export interface CrudFeatures { * Default: `undefined` */ emptyValue: undefined | null; + /** * Does the connector support using transactions for performing CRUD * operations atomically and being able to commit or rollback the changes? @@ -55,6 +56,14 @@ export interface CrudFeatures { * Default: `false` */ supportsTransactions: boolean; + + /** + * Does the database use string and objectId as type of id? + * MongoDB use this format. + * + * Default: `false` + */ + convertIdType: boolean; } /** diff --git a/packages/repository/src/__tests__/acceptance/belongs-to.relation.acceptance.ts b/packages/repository/src/__tests__/acceptance/belongs-to.relation.acceptance.ts deleted file mode 100644 index 4142e36e0f9f..000000000000 --- a/packages/repository/src/__tests__/acceptance/belongs-to.relation.acceptance.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {Application} from '@loopback/core'; -import {expect} from '@loopback/testlab'; -import { - ApplicationWithRepositories, - juggler, - repository, - RepositoryMixin, -} from '../..'; -import { - CustomerRepository, - OrderRepository, - ShipmentRepository, -} from '../fixtures/repositories'; - -describe('BelongsTo relation', () => { - // Given a Customer and Order models - see definitions at the bottom - - let app: ApplicationWithRepositories; - let controller: OrderController; - let customerRepo: CustomerRepository; - let orderRepo: OrderRepository; - let shipmentRepo: ShipmentRepository; - - before(givenApplicationWithMemoryDB); - before(givenBoundCrudRepositories); - before(givenOrderController); - - beforeEach(async () => { - await orderRepo.deleteAll(); - }); - - it('can find customer of order', async () => { - const customer = await customerRepo.create({name: 'Order McForder'}); - const order = await orderRepo.create({ - customerId: customer.id, - description: 'Order from Order McForder, the hoarder of Mordor', - }); - const result = await controller.findOwnerOfOrder(order.id); - expect(result).to.deepEqual(customer); - }); - - it('can find shipment of order with a custom foreign key name', async () => { - const shipment = await shipmentRepo.create({ - name: 'Tuesday morning shipment', - }); - const order = await orderRepo.create({ - // eslint-disable-next-line @typescript-eslint/camelcase - shipment_id: shipment.id, - description: 'Order that is shipped Tuesday morning', - }); - const result = await controller.findOrderShipment(order.id); - expect(result).to.deepEqual(shipment); - }); - - //--- HELPERS ---// - - class OrderController { - constructor( - @repository(OrderRepository) protected orderRepository: OrderRepository, - ) {} - - async findOwnerOfOrder(orderId: string) { - return this.orderRepository.customer(orderId); - } - - async findOrderShipment(orderId: string) { - return this.orderRepository.shipment(orderId); - } - } - - function givenApplicationWithMemoryDB() { - class TestApp extends RepositoryMixin(Application) {} - app = new TestApp(); - app.dataSource(new juggler.DataSource({name: 'db', connector: 'memory'})); - } - - async function givenBoundCrudRepositories() { - app.repository(CustomerRepository); - app.repository(OrderRepository); - app.repository(ShipmentRepository); - customerRepo = await app.getRepository(CustomerRepository); - orderRepo = await app.getRepository(OrderRepository); - shipmentRepo = await app.getRepository(ShipmentRepository); - } - - async function givenOrderController() { - app.controller(OrderController); - controller = await app.get('controllers.OrderController'); - } -}); diff --git a/packages/repository/src/__tests__/acceptance/has-many-without-di.relation.acceptance.ts b/packages/repository/src/__tests__/acceptance/has-many-without-di.relation.acceptance.ts deleted file mode 100644 index 60a50e01ea7d..000000000000 --- a/packages/repository/src/__tests__/acceptance/has-many-without-di.relation.acceptance.ts +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {expect} from '@loopback/testlab'; -import { - DefaultCrudRepository, - Entity, - EntityCrudRepository, - Getter, - hasMany, - HasManyRepositoryFactory, - juggler, - model, - property, -} from '../..'; - -describe('HasMany relation', () => { - // Given a Customer and Order models - see definitions at the bottom - - let existingCustomerId: number; - let ds: juggler.DataSource; - let customerRepo: CustomerRepository; - let orderRepo: OrderRepository; - - before(givenDataSource); - before(givenOrderRepository); - before(givenCustomerRepository); - beforeEach(async () => { - await orderRepo.deleteAll(); - existingCustomerId = (await givenPersistedCustomerInstance()).id; - }); - - it('can create an instance of the related model', async () => { - async function createCustomerOrders( - customerId: number, - orderData: Partial, - ): Promise { - return customerRepo.orders(customerId).create(orderData); - } - const order = await createCustomerOrders(existingCustomerId, { - description: 'order 1', - }); - expect(order.toObject()).to.containDeep({ - customerId: existingCustomerId, - description: 'order 1', - }); - - const persisted = await orderRepo.findById(order.id); - expect(persisted.toObject()).to.deepEqual(order.toObject()); - }); - - it('can find instances of the related model', async () => { - async function createCustomerOrders( - customerId: number, - orderData: Partial, - ): Promise { - return customerRepo.orders(customerId).create(orderData); - } - async function findCustomerOrders(customerId: number) { - return customerRepo.orders(customerId).find(); - } - - const order = await createCustomerOrders(existingCustomerId, { - description: 'order 1', - }); - const notMyOrder = await createCustomerOrders(existingCustomerId + 1, { - description: 'order 2', - }); - const orders = await findCustomerOrders(existingCustomerId); - - expect(orders).to.containEql(order); - expect(orders).to.not.containEql(notMyOrder); - - const persisted = await orderRepo.find({ - where: {customerId: existingCustomerId}, - }); - expect(persisted).to.deepEqual(orders); - }); - - //--- HELPERS ---// - - @model() - class Order extends Entity { - @property({ - type: 'number', - id: true, - }) - id: number; - - @property({ - type: 'string', - required: true, - }) - description: string; - - @property({ - type: 'number', - required: true, - }) - customerId: number; - } - - @model() - class Customer extends Entity { - @property({ - type: 'number', - id: true, - }) - id: number; - - @property({ - type: 'string', - }) - name: string; - - @hasMany(() => Order) - orders: Order[]; - } - - class OrderRepository extends DefaultCrudRepository< - Order, - typeof Order.prototype.id - > { - constructor(db: juggler.DataSource) { - super(Order, db); - } - } - - class CustomerRepository extends DefaultCrudRepository< - Customer, - typeof Customer.prototype.id - > { - public readonly orders: HasManyRepositoryFactory< - Order, - typeof Customer.prototype.id - >; - - constructor( - protected db: juggler.DataSource, - orderRepositoryGetter: Getter< - EntityCrudRepository - >, - ) { - super(Customer, db); - this.orders = this._createHasManyRepositoryFactoryFor( - 'orders', - orderRepositoryGetter, - ); - } - } - - function givenDataSource() { - ds = new juggler.DataSource({connector: 'memory'}); - } - - function givenOrderRepository() { - orderRepo = new OrderRepository(ds); - } - - function givenCustomerRepository() { - customerRepo = new CustomerRepository(ds, Getter.fromValue(orderRepo)); - } - - async function givenPersistedCustomerInstance() { - return customerRepo.create({name: 'a customer'}); - } -}); diff --git a/packages/repository/src/__tests__/acceptance/has-many.relation.acceptance.ts b/packages/repository/src/__tests__/acceptance/has-many.relation.acceptance.ts deleted file mode 100644 index 8337baa60c8a..000000000000 --- a/packages/repository/src/__tests__/acceptance/has-many.relation.acceptance.ts +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {Application} from '@loopback/core'; -import {expect} from '@loopback/testlab'; -import * as _ from 'lodash'; -import { - ApplicationWithRepositories, - juggler, - repository, - RepositoryMixin, -} from '../..'; -import {Customer, Order} from '../fixtures/models'; -import {CustomerRepository, OrderRepository} from '../fixtures/repositories'; - -describe('HasMany relation', () => { - // Given a Customer and Order models - see definitions at the bottom - - let app: ApplicationWithRepositories; - let controller: CustomerController; - let customerRepo: CustomerRepository; - let orderRepo: OrderRepository; - let existingCustomerId: number; - - before(givenApplicationWithMemoryDB); - before(givenBoundCrudRepositoriesForCustomerAndOrder); - before(givenCustomerController); - - beforeEach(async () => { - await orderRepo.deleteAll(); - }); - - beforeEach(async () => { - existingCustomerId = (await givenPersistedCustomerInstance()).id; - }); - - it('can create an instance of the related model', async () => { - const order = await controller.createCustomerOrders(existingCustomerId, { - description: 'order 1', - }); - expect(order.toObject()).to.containDeep({ - customerId: existingCustomerId, - description: 'order 1', - }); - - const persisted = await orderRepo.findById(order.id); - expect(persisted.toObject()).to.deepEqual(order.toObject()); - }); - - it('can find instances of the related model', async () => { - const order = await controller.createCustomerOrders(existingCustomerId, { - description: 'order 1', - }); - const notMyOrder = await controller.createCustomerOrders( - existingCustomerId + 1, - { - description: 'order 2', - }, - ); - const foundOrders = await controller.findCustomerOrders(existingCustomerId); - expect(foundOrders).to.containEql(order); - expect(foundOrders).to.not.containEql(notMyOrder); - - const persisted = await orderRepo.find({ - where: {customerId: existingCustomerId}, - }); - expect(persisted).to.deepEqual(foundOrders); - }); - - it('can patch many instances', async () => { - await controller.createCustomerOrders(existingCustomerId, { - description: 'order 1', - isShipped: false, - }); - await controller.createCustomerOrders(existingCustomerId, { - description: 'order 2', - isShipped: false, - }); - const patchObject = {isShipped: true}; - const arePatched = await controller.patchCustomerOrders( - existingCustomerId, - patchObject, - ); - expect(arePatched.count).to.equal(2); - const patchedData = _.map( - await controller.findCustomerOrders(existingCustomerId), - d => _.pick(d, ['customerId', 'description', 'isShipped']), - ); - expect(patchedData).to.eql([ - { - customerId: existingCustomerId, - description: 'order 1', - isShipped: true, - }, - { - customerId: existingCustomerId, - description: 'order 2', - isShipped: true, - }, - ]); - }); - - it('throws error when query tries to change the foreignKey', async () => { - await expect( - controller.patchCustomerOrders(existingCustomerId, { - customerId: existingCustomerId + 1, - }), - ).to.be.rejectedWith(/Property "customerId" cannot be changed!/); - }); - - it('can delete many instances', async () => { - await controller.createCustomerOrders(existingCustomerId, { - description: 'order 1', - }); - await controller.createCustomerOrders(existingCustomerId, { - description: 'order 2', - }); - const deletedOrders = await controller.deleteCustomerOrders( - existingCustomerId, - ); - expect(deletedOrders.count).to.equal(2); - const relatedOrders = await controller.findCustomerOrders( - existingCustomerId, - ); - expect(relatedOrders).to.be.empty(); - }); - - it("does not delete instances that don't belong to the constrained instance", async () => { - const newOrder = { - customerId: existingCustomerId + 1, - description: 'another order', - }; - await orderRepo.create(newOrder); - await controller.deleteCustomerOrders(existingCustomerId); - const orders = await orderRepo.find(); - expect(orders).to.have.length(1); - expect(_.pick(orders[0], ['customerId', 'description'])).to.eql(newOrder); - }); - - it('does not create an array of the related model', async () => { - await expect( - customerRepo.create({ - name: 'a customer', - orders: [ - { - description: 'order 1', - }, - ], - }), - ).to.be.rejectedWith(/`orders` is not defined/); - }); - - context('when targeting the source model', () => { - it('gets the parent entity through the child entity', async () => { - const parent = await customerRepo.create({name: 'parent customer'}); - const child = await customerRepo.create({ - name: 'child customer', - parentId: parent.id, - }); - - const childsParent = await controller.getParentCustomer(child.id); - - expect(_.pick(childsParent, ['id', 'name'])).to.eql( - _.pick(parent, ['id', 'name']), - ); - }); - - it('creates a child entity through the parent entity', async () => { - const parent = await customerRepo.create({ - name: 'parent customer', - }); - const child = await controller.createCustomerChildren(parent.id, { - name: 'child customer', - }); - expect(child.parentId).to.equal(parent.id); - - const children = await controller.findCustomerChildren(parent.id); - expect(children).to.containEql(child); - }); - }); - - // This should be enforced by the database to avoid race conditions - it.skip('reject create request when the customer does not exist'); - - class CustomerController { - constructor( - @repository(CustomerRepository) - protected customerRepository: CustomerRepository, - ) {} - - async createCustomerOrders( - customerId: number, - orderData: Partial, - ): Promise { - return this.customerRepository.orders(customerId).create(orderData); - } - - async findCustomerOrders(customerId: number) { - return this.customerRepository.orders(customerId).find(); - } - - async patchCustomerOrders(customerId: number, order: Partial) { - return this.customerRepository.orders(customerId).patch(order); - } - - async deleteCustomerOrders(customerId: number) { - return this.customerRepository.orders(customerId).delete(); - } - - async getParentCustomer(customerId: number) { - return this.customerRepository.parent(customerId); - } - - async createCustomerChildren( - customerId: number, - customerData: Partial, - ) { - return this.customerRepository.customers(customerId).create(customerData); - } - - async findCustomerChildren(customerId: number) { - return this.customerRepository.customers(customerId).find(); - } - } - - function givenApplicationWithMemoryDB() { - class TestApp extends RepositoryMixin(Application) {} - - app = new TestApp(); - app.dataSource(new juggler.DataSource({name: 'db', connector: 'memory'})); - } - - async function givenBoundCrudRepositoriesForCustomerAndOrder() { - app.repository(CustomerRepository); - app.repository(OrderRepository); - customerRepo = await app.getRepository(CustomerRepository); - orderRepo = await app.getRepository(OrderRepository); - } - - async function givenCustomerController() { - app.controller(CustomerController); - controller = await app.get( - 'controllers.CustomerController', - ); - } - - async function givenPersistedCustomerInstance() { - return customerRepo.create({name: 'a customer'}); - } -}); diff --git a/packages/repository/src/__tests__/acceptance/has-one.relation.acceptance.ts b/packages/repository/src/__tests__/acceptance/has-one.relation.acceptance.ts deleted file mode 100644 index 3c50eebd3325..000000000000 --- a/packages/repository/src/__tests__/acceptance/has-one.relation.acceptance.ts +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {Application} from '@loopback/core'; -import {expect, toJSON} from '@loopback/testlab'; -import { - ApplicationWithRepositories, - EntityNotFoundError, - Filter, - juggler, - repository, - RepositoryMixin, -} from '../..'; -import {Address} from '../fixtures/models'; -import {AddressRepository, CustomerRepository} from '../fixtures/repositories'; - -describe('hasOne relation', () => { - // Given a Customer and Address models - see definitions at the bottom - - let app: ApplicationWithRepositories; - let controller: CustomerController; - let customerRepo: CustomerRepository; - let addressRepo: AddressRepository; - let existingCustomerId: number; - - before(givenApplicationWithMemoryDB); - before(givenBoundCrudRepositoriesForCustomerAndAddress); - before(givenCustomerController); - - beforeEach(async () => { - await addressRepo.deleteAll(); - existingCustomerId = (await givenPersistedCustomerInstance()).id; - }); - - it('can create an instance of the related model', async () => { - const address = await controller.createCustomerAddress(existingCustomerId, { - street: '123 test avenue', - }); - expect(address.toObject()).to.containDeep({ - customerId: existingCustomerId, - street: '123 test avenue', - }); - - const persisted = await addressRepo.findById(address.zipcode); - expect(persisted.toObject()).to.deepEqual(address.toObject()); - }); - - // We do not enforce referential integrity at the moment. It is up to - // our users to set up unique constraint(s) between related models at the - // database level - it.skip('refuses to create related model instance twice', async () => { - const address = await controller.createCustomerAddress(existingCustomerId, { - street: '123 test avenue', - }); - await expect( - controller.createCustomerAddress(existingCustomerId, { - street: '456 test street', - }), - ).to.be.rejectedWith(/Duplicate entry for Address.customerId/); - expect(address.toObject()).to.containDeep({ - customerId: existingCustomerId, - street: '123 test avenue', - }); - - const persisted = await addressRepo.findById(address.zipcode); - expect(persisted.toObject()).to.deepEqual(address.toObject()); - }); - - it('can find instance of the related model', async () => { - const address = await controller.createCustomerAddress(existingCustomerId, { - street: '123 test avenue', - }); - const foundAddress = await controller.findCustomerAddress( - existingCustomerId, - ); - expect(foundAddress).to.containEql(address); - expect(toJSON(foundAddress)).to.deepEqual(toJSON(address)); - - const persisted = await addressRepo.find({ - where: {customerId: existingCustomerId}, - }); - expect(persisted[0]).to.deepEqual(foundAddress); - }); - - // FIXME(b-admike): make sure the test fails with compiler error - it.skip('ignores where filter to find related model instance', async () => { - const foundAddress = await controller.findCustomerAddressWithFilter( - existingCustomerId, - // the compiler should complain that the where field is - // not accepted in the filter object for the get() method - // if the following line is uncommented - { - where: {street: '456 test road'}, - }, - ); - - const persisted = await addressRepo.find({ - where: {customerId: existingCustomerId}, - }); - // TODO: make sure this test fails when where condition is supplied - // compiler should have errored out (?) - expect(persisted[0]).to.deepEqual(foundAddress); - }); - - it('reports EntityNotFound error when related model is deleted', async () => { - const address = await controller.createCustomerAddress(existingCustomerId, { - street: '123 test avenue', - }); - await addressRepo.deleteById(address.zipcode); - - await expect( - controller.findCustomerAddress(existingCustomerId), - ).to.be.rejectedWith(EntityNotFoundError); - }); - - it('can PATCH hasOne instances', async () => { - const address = await controller.createCustomerAddress(existingCustomerId, { - street: '1 Amedee Bonnet', - zipcode: '69740', - city: 'Genas', - province: 'Rhone', - }); - - const patchObject = {city: 'Lyon-Genas'}; - const arePatched = await controller.patchCustomerAddress( - existingCustomerId, - patchObject, - ); - - expect(arePatched).to.deepEqual({count: 1}); - const patchedData = await addressRepo.findById(address.zipcode); - expect(toJSON(patchedData)).to.deepEqual({ - customerId: existingCustomerId, - street: '1 Amedee Bonnet', - zipcode: '69740', - city: 'Lyon-Genas', - province: 'Rhone', - }); - }); - - it('patches the related instance only', async () => { - const bob = await customerRepo.create({name: 'Bob'}); - await customerRepo.address(bob.id).create({city: 'Paris'}); - - const alice = await customerRepo.create({name: 'Alice'}); - await customerRepo.address(alice.id).create({city: 'London'}); - - const result = await controller.patchCustomerAddress(alice.id, { - city: 'New York', - }); - - expect(result).to.deepEqual({count: 1}); - - const found = await customerRepo.address(bob.id).get(); - expect(toJSON(found)).to.containDeep({city: 'Paris'}); - }); - - it('throws an error when PATCH tries to change the foreignKey', async () => { - await expect( - controller.patchCustomerAddress(existingCustomerId, { - customerId: existingCustomerId + 1, - }), - ).to.be.rejectedWith(/Property "customerId" cannot be changed!/); - }); - - it('can DELETE hasOne relation instances', async () => { - await controller.createCustomerAddress(existingCustomerId, { - street: '1 Amedee Bonnet', - zipcode: '69740', - city: 'Genas', - province: 'Rhone', - }); - - const areDeleted = await controller.deleteCustomerAddress( - existingCustomerId, - ); - expect(areDeleted).to.deepEqual({count: 1}); - - await expect( - controller.findCustomerAddress(existingCustomerId), - ).to.be.rejectedWith(EntityNotFoundError); - }); - - it('deletes the related model instance only', async () => { - const bob = await customerRepo.create({name: 'Bob'}); - await customerRepo.address(bob.id).create({city: 'Paris'}); - - const alice = await customerRepo.create({name: 'Alice'}); - await customerRepo.address(alice.id).create({city: 'London'}); - - const result = await controller.deleteCustomerAddress(alice.id); - - expect(result).to.deepEqual({count: 1}); - - const found = await addressRepo.find(); - expect(found).to.have.length(1); - }); - - /*---------------- HELPERS -----------------*/ - - class CustomerController { - constructor( - @repository(CustomerRepository) - protected customerRepository: CustomerRepository, - ) {} - - async createCustomerAddress( - customerId: number, - addressData: Partial
, - ): Promise
{ - return this.customerRepository.address(customerId).create(addressData); - } - - async findCustomerAddress(customerId: number) { - return this.customerRepository.address(customerId).get(); - } - - async findCustomerAddressWithFilter( - customerId: number, - filter: Filter
, - ) { - return this.customerRepository.address(customerId).get(filter); - } - async patchCustomerAddress( - customerId: number, - addressData: Partial
, - ) { - return this.customerRepository.address(customerId).patch(addressData); - } - - async deleteCustomerAddress(customerId: number) { - return this.customerRepository.address(customerId).delete(); - } - } - - function givenApplicationWithMemoryDB() { - class TestApp extends RepositoryMixin(Application) {} - app = new TestApp(); - app.dataSource(new juggler.DataSource({name: 'db', connector: 'memory'})); - } - - async function givenBoundCrudRepositoriesForCustomerAndAddress() { - app.repository(CustomerRepository); - app.repository(AddressRepository); - customerRepo = await app.getRepository(CustomerRepository); - addressRepo = await app.getRepository(AddressRepository); - } - - async function givenCustomerController() { - app.controller(CustomerController); - controller = await app.get( - 'controllers.CustomerController', - ); - } - - async function givenPersistedCustomerInstance() { - return customerRepo.create({name: 'a customer'}); - } -}); diff --git a/packages/repository/src/__tests__/acceptance/repository.acceptance.ts b/packages/repository/src/__tests__/acceptance/repository.acceptance.ts deleted file mode 100644 index b6d68dc7f816..000000000000 --- a/packages/repository/src/__tests__/acceptance/repository.acceptance.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {expect} from '@loopback/testlab'; -import {DataSource} from 'loopback-datasource-juggler'; -import {AnyObject, DefaultCrudRepository, Entity, model, property} from '../..'; -import {Product} from '../fixtures/models/product.model'; -import {ProductRepository} from '../fixtures/repositories/product.repository'; - -// This test shows the recommended way how to use @loopback/repository -// together with existing connectors when building LoopBack applications -describe('Repository in Thinking in LoopBack', () => { - let repo: ProductRepository; - beforeEach(givenProductRepository); - - it('counts models in empty database', async () => { - expect(await repo.count()).to.deepEqual({count: 0}); - }); - - it('creates a new model', async () => { - const p: Product = await repo.create({name: 'Ink Pen', slug: 'pen'}); - expect(p).instanceof(Product); - expect.exists(p.id); - }); - - it('can save a model', async () => { - const p = await repo.create({slug: 'pencil'}); - - p.name = 'Red Pencil'; - await repo.save(p); - - await repo.findById(p.id); - expect(p).to.have.properties({ - slug: 'pencil', - name: 'Red Pencil', - }); - }); - - it('rejects extra model properties (defaults to strict mode)', async () => { - await expect( - repo.create({name: 'custom', extra: 'additional-data'} as AnyObject), - ).to.be.rejectedWith(/extra.*not defined/); - }); - - it('allows models to allow additional properties', async () => { - // TODO(bajtos) Add syntactic sugar to allow the following usage: - // @model({strict: false}) - @model({settings: {strict: false}}) - class Flexible extends Entity { - @property({id: true}) - id: number; - } - - const flexibleRepo = new DefaultCrudRepository< - Flexible, - typeof Flexible.prototype.id - >(Flexible, new DataSource({connector: 'memory'})); - - const created = await flexibleRepo.create({ - extra: 'additional data', - } as AnyObject); - const stored = await flexibleRepo.findById(created.id); - expect(stored).to.containDeep({extra: 'additional data'}); - }); - - it('allows models to allow nested model properties', async () => { - @model() - class Role extends Entity { - @property() - name: string; - } - - @model() - class Address extends Entity { - @property() - street: string; - } - - @model() - class User extends Entity { - @property({ - type: 'number', - id: true, - }) - id: number; - - @property({type: 'string'}) - name: string; - - @property.array(Role) - roles: Role[]; - - @property() - address: Address; - } - - const userRepo = new DefaultCrudRepository( - User, - new DataSource({connector: 'memory'}), - ); - - const user = { - id: 1, - name: 'foo', - roles: [{name: 'admin'}, {name: 'user'}], - address: {street: 'backstreet'}, - }; - const created = await userRepo.create(user); - - const stored = await userRepo.findById(created.id); - expect(stored).to.containDeep(user); - }); - - function givenProductRepository() { - const db = new DataSource({ - connector: 'memory', - }); - repo = new ProductRepository(db); - } -}); diff --git a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts b/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts deleted file mode 100644 index e347ae765f08..000000000000 --- a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {expect} from '@loopback/testlab'; -import { - BelongsToAccessor, - BelongsToDefinition, - createBelongsToAccessor, - createHasManyRepositoryFactory, - DefaultCrudRepository, - Entity, - EntityCrudRepository, - EntityNotFoundError, - Getter, - HasManyDefinition, - HasManyRepository, - HasManyRepositoryFactory, - juggler, - ModelDefinition, - RelationType, -} from '../../..'; - -// Given a Customer and Order models - see definitions at the bottom -let db: juggler.DataSource; -let customerRepo: EntityCrudRepository; -let orderRepo: EntityCrudRepository; -let reviewRepo: EntityCrudRepository; - -describe('HasMany relation', () => { - let existingCustomerId: number; - - let customerOrderRepo: HasManyRepository; - let customerAuthoredReviewFactoryFn: HasManyRepositoryFactory< - Review, - typeof Customer.prototype.id - >; - let customerApprovedReviewFactoryFn: HasManyRepositoryFactory< - Review, - typeof Customer.prototype.id - >; - - before(givenCrudRepositories); - before(givenPersistedCustomerInstance); - before(givenConstrainedRepositories); - before(givenRepositoryFactoryFunctions); - - beforeEach(async function resetDatabase() { - await orderRepo.deleteAll(); - await reviewRepo.deleteAll(); - }); - - it('can create an instance of the related model', async () => { - const order = await customerOrderRepo.create({ - description: 'an order desc', - }); - const persisted = await orderRepo.findById(order.id); - - expect(order).to.deepEqual(persisted); - }); - - it('can find an instance of the related model', async () => { - const order = await customerOrderRepo.create({ - description: 'an order desc', - }); - const notMyOrder = await orderRepo.create({ - description: "someone else's order desc", - customerId: existingCustomerId + 1, // a different customerId, - }); - const persistedOrders = await orderRepo.find({ - where: { - customerId: existingCustomerId, - }, - }); - - const orders = await customerOrderRepo.find(); - - expect(orders).to.containEql(order); - expect(orders).to.not.containEql(notMyOrder); - expect(orders).to.deepEqual(persistedOrders); - }); - - it('finds appropriate related model instances for multiple relations', async () => { - // note(shimks): roundabout way of creating reviews with 'approves' - // ideally, the review repository should have a approve function - // which should 'approve' a review - // On another note, this test should be separated for 'create' and 'find' - await customerAuthoredReviewFactoryFn(existingCustomerId).create({ - description: 'my wonderful review', - approvedId: existingCustomerId + 1, - }); - await customerAuthoredReviewFactoryFn(existingCustomerId + 1).create({ - description: 'smash that progenitor loving approve button', - approvedId: existingCustomerId, - }); - - const reviewsApprovedByCustomerOne = await customerApprovedReviewFactoryFn( - existingCustomerId, - ).find(); - const reviewsApprovedByCustomerTwo = await customerApprovedReviewFactoryFn( - existingCustomerId + 1, - ).find(); - - const persistedReviewsApprovedByCustomerOne = await reviewRepo.find({ - where: { - approvedId: existingCustomerId, - }, - }); - const persistedReviewsApprovedByCustomerTwo = await reviewRepo.find({ - where: { - approvedId: existingCustomerId + 1, - }, - }); - - expect(reviewsApprovedByCustomerOne).to.eql( - persistedReviewsApprovedByCustomerOne, - ); - expect(reviewsApprovedByCustomerTwo).to.eql( - persistedReviewsApprovedByCustomerTwo, - ); - }); - - //--- HELPERS ---// - - async function givenPersistedCustomerInstance() { - const customer = await customerRepo.create({name: 'a customer'}); - existingCustomerId = customer.id; - } - - function givenConstrainedRepositories() { - const orderFactoryFn = createHasManyRepositoryFactory< - Order, - typeof Order.prototype.id, - typeof Customer.prototype.id - >( - Customer.definition.relations.orders as HasManyDefinition, - Getter.fromValue(orderRepo), - ); - - customerOrderRepo = orderFactoryFn(existingCustomerId); - } - - function givenRepositoryFactoryFunctions() { - customerAuthoredReviewFactoryFn = createHasManyRepositoryFactory( - Customer.definition.relations.reviewsAuthored as HasManyDefinition, - Getter.fromValue(reviewRepo), - ); - customerApprovedReviewFactoryFn = createHasManyRepositoryFactory( - Customer.definition.relations.reviewsApproved as HasManyDefinition, - Getter.fromValue(reviewRepo), - ); - } -}); - -describe('BelongsTo relation', () => { - let findCustomerOfOrder: BelongsToAccessor< - Customer, - typeof Order.prototype.id - >; - - before(givenCrudRepositories); - before(givenAccessor); - beforeEach(async function resetDatabase() { - await Promise.all([ - customerRepo.deleteAll(), - orderRepo.deleteAll(), - reviewRepo.deleteAll(), - ]); - }); - - it('finds an instance of the related model', async () => { - const customer = await customerRepo.create({name: 'Order McForder'}); - const order = await orderRepo.create({ - customerId: customer.id, - description: 'Order from Order McForder, the hoarder of Mordor', - }); - - const result = await findCustomerOfOrder(order.id); - - expect(result).to.deepEqual(customer); - }); - - it('throws EntityNotFound error when the related model does not exist', async () => { - const order = await orderRepo.create({ - customerId: 999, // does not exist - description: 'Order of a fictional customer', - }); - - await expect(findCustomerOfOrder(order.id)).to.be.rejectedWith( - EntityNotFoundError, - ); - }); - - //--- HELPERS ---// - - function givenAccessor() { - findCustomerOfOrder = createBelongsToAccessor( - Order.definition.relations.customer as BelongsToDefinition, - Getter.fromValue(customerRepo), - orderRepo, - ); - } -}); - -//--- HELPERS ---// - -class Order extends Entity { - id: number; - description: string; - customerId: number; - - static definition = new ModelDefinition('Order') - .addProperty('id', {type: 'number', id: true}) - .addProperty('description', {type: 'string', required: true}) - .addProperty('customerId', {type: 'number', required: true}) - .addRelation({ - name: 'customer', - type: RelationType.belongsTo, - source: Order, - target: () => Customer, - keyFrom: 'customerId', - keyTo: 'id', - }); -} - -class Review extends Entity { - id: number; - description: string; - authorId: number; - approvedId: number; - - static definition = new ModelDefinition('Review') - .addProperty('id', {type: 'number', id: true}) - .addProperty('description', {type: 'string', required: true}) - .addProperty('authorId', {type: 'number', required: false}) - .addProperty('approvedId', {type: 'number', required: false}); -} - -class Customer extends Entity { - id: number; - name: string; - orders: Order[]; - reviewsAuthored: Review[]; - reviewsApproved: Review[]; - - static definition: ModelDefinition = new ModelDefinition('Customer') - .addProperty('id', {type: 'number', id: true}) - .addProperty('name', {type: 'string', required: true}) - .addProperty('orders', {type: Order, array: true}) - .addProperty('reviewsAuthored', {type: Review, array: true}) - .addProperty('reviewsApproved', {type: Review, array: true}) - .addRelation({ - name: 'orders', - type: RelationType.hasMany, - targetsMany: true, - source: Customer, - target: () => Order, - keyTo: 'customerId', - }) - .addRelation({ - name: 'reviewsAuthored', - type: RelationType.hasMany, - targetsMany: true, - source: Customer, - target: () => Review, - keyTo: 'authorId', - }) - .addRelation({ - name: 'reviewsApproved', - type: RelationType.hasMany, - targetsMany: true, - source: Customer, - target: () => Review, - keyTo: 'approvedId', - }); -} - -function givenCrudRepositories() { - db = new juggler.DataSource({connector: 'memory'}); - - customerRepo = new DefaultCrudRepository(Customer, db); - orderRepo = new DefaultCrudRepository(Order, db); - reviewRepo = new DefaultCrudRepository(Review, db); -}