diff --git a/package.json b/package.json index 0df99de..0375f3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@extollo/lib", - "version": "0.14.5", + "version": "0.14.6", "description": "The framework library that lifts up your code.", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -42,6 +42,8 @@ "pug": "^3.0.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", + "sqlite": "^4.1.2", + "sqlite3": "^5.1.1", "ssh2": "^1.1.0", "ts-node": "^9.1.1", "typedoc": "^0.20.36", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40e02c4..02efddc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,8 @@ specifiers: reflect-metadata: ^0.1.13 rimraf: ^3.0.2 sinon: ^12.0.1 + sqlite: ^4.1.2 + sqlite3: ^5.1.1 ssh2: ^1.1.0 ts-node: ^9.1.1 typedoc: ^0.20.36 @@ -91,6 +93,8 @@ dependencies: pug: 3.0.2 reflect-metadata: 0.1.13 rimraf: 3.0.2 + sqlite: 4.1.2 + sqlite3: 5.1.1 ssh2: 1.1.0 ts-node: 9.1.1_typescript@4.7.4 typedoc: 0.20.36_typescript@4.7.4 @@ -227,6 +231,11 @@ packages: - '@types/node' dev: false + /@gar/promisify/1.1.3: + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + dev: false + optional: true + /@mapbox/node-pre-gyp/1.0.5: resolution: {integrity: sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==} hasBin: true @@ -239,7 +248,7 @@ packages: npmlog: 4.1.2 rimraf: 3.0.2 semver: 7.3.5 - tar: 6.1.0 + tar: 6.1.11 transitivePeerDependencies: - supports-color dev: false @@ -265,6 +274,23 @@ packages: fastq: 1.11.0 dev: true + /@npmcli/fs/1.1.1: + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.3.5 + dev: false + optional: true + + /@npmcli/move-file/1.1.2: + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + dev: false + optional: true + /@popperjs/core/2.10.2: resolution: {integrity: sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==} dev: false @@ -299,6 +325,12 @@ packages: resolution: {integrity: sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==} dev: true + /@tootallnate/once/1.1.2: + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + dev: false + optional: true + /@tsconfig/node10/1.0.8: resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==} dev: false @@ -609,10 +641,31 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.1 + debug: 4.3.2 + transitivePeerDependencies: + - supports-color + dev: false + + /agentkeepalive/4.2.1: + resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==} + engines: {node: '>= 8.0.0'} + dependencies: + debug: 4.3.2 + depd: 1.1.2 + humanize-ms: 1.2.1 transitivePeerDependencies: - supports-color dev: false + optional: true + + /aggregate-error/3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: false + optional: true /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -652,7 +705,7 @@ packages: dev: false /ansi-regex/2.1.1: - resolution: {integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8=} + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} engines: {node: '>=0.10.0'} dev: false @@ -660,6 +713,12 @@ packages: resolution: {integrity: sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==} engines: {node: '>=8'} + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + optional: true + /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -692,6 +751,15 @@ packages: readable-stream: 2.3.7 dev: false + /are-we-there-yet/3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.0 + dev: false + optional: true + /arg/4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: false @@ -839,6 +907,33 @@ packages: dicer: 0.3.0 dev: false + /cacache/15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.1.7 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.1.3 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.1.11 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + dev: false + optional: true + /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -918,6 +1013,12 @@ packages: engines: {node: '>=10'} dev: false + /clean-stack/2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: false + optional: true + /cli-cursor/3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -961,7 +1062,7 @@ packages: dev: false /code-point-at/1.1.0: - resolution: {integrity: sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=} + resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} engines: {node: '>=0.10.0'} dev: false @@ -984,6 +1085,12 @@ packages: /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /color-support/1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + optional: true + /colors/1.0.3: resolution: {integrity: sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=} engines: {node: '>=0.1.90'} @@ -1002,7 +1109,7 @@ packages: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} /console-control-strings/1.1.0: - resolution: {integrity: sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=} + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: false /constantinople/4.0.1: @@ -1018,7 +1125,7 @@ packages: dev: false /core-util-is/1.0.2: - resolution: {integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=} + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} dev: false /cpu-features/0.0.2: @@ -1058,6 +1165,7 @@ packages: optional: true dependencies: ms: 2.1.2 + dev: true /debug/4.3.2: resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} @@ -1084,6 +1192,19 @@ packages: supports-color: 8.1.1 dev: true + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + optional: true + /decamelize/4.0.0: resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} engines: {node: '>=10'} @@ -1107,7 +1228,7 @@ packages: dev: false /delegates/1.0.0: - resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=} + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false /denque/1.5.0: @@ -1115,8 +1236,14 @@ packages: engines: {node: '>=0.10'} dev: false + /depd/1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: false + optional: true + /detect-libc/1.0.3: - resolution: {integrity: sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=} + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} hasBin: true dev: false @@ -1175,6 +1302,14 @@ packages: /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /encoding/0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + requiresBuild: true + dependencies: + iconv-lite: 0.6.3 + dev: false + optional: true + /enquirer/2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} engines: {node: '>=8.6'} @@ -1182,6 +1317,17 @@ packages: ansi-colors: 4.1.1 dev: true + /env-paths/2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: false + optional: true + + /err-code/2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + dev: false + optional: true + /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -1461,7 +1607,7 @@ packages: dev: true /gauge/2.7.4: - resolution: {integrity: sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=} + resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==} dependencies: aproba: 1.2.0 console-control-strings: 1.1.0 @@ -1473,6 +1619,21 @@ packages: wide-align: 1.1.3 dev: false + /gauge/4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + aproba: 1.2.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + optional: true + /get-caller-file/2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1570,7 +1731,7 @@ packages: dev: false /has-unicode/2.0.1: - resolution: {integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=} + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false /has/1.0.3: @@ -1585,16 +1746,40 @@ packages: hasBin: true dev: true + /http-cache-semantics/4.1.0: + resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} + dev: false + optional: true + + /http-proxy-agent/4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.3.2 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + /https-proxy-agent/5.0.0: resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==} engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.1 + debug: 4.3.2 transitivePeerDependencies: - supports-color dev: false + /humanize-ms/1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + dependencies: + ms: 2.1.3 + dev: false + optional: true + /iconv-lite/0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1602,6 +1787,14 @@ packages: safer-buffer: 2.1.2 dev: false + /iconv-lite/0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + optional: true + /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -1627,7 +1820,17 @@ packages: /imurmurhash/0.1.4: resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=} engines: {node: '>=0.8.19'} - dev: true + + /indent-string/4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: false + optional: true + + /infer-owner/1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + dev: false + optional: true /inflight/1.0.6: resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} @@ -1681,6 +1884,11 @@ packages: - supports-color dev: false + /ip/2.0.0: + resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + dev: false + optional: true + /is-binary-path/2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1707,7 +1915,7 @@ packages: dev: true /is-fullwidth-code-point/1.0.0: - resolution: {integrity: sha1-754xOG8DGn8NZDr4L95QxFfvAMs=} + resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} engines: {node: '>=0.10.0'} dependencies: number-is-nan: 1.0.1 @@ -1729,6 +1937,11 @@ packages: engines: {node: '>=8'} dev: false + /is-lambda/1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + dev: false + optional: true + /is-number/7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1760,12 +1973,11 @@ packages: dev: true /isarray/1.0.0: - resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false /isexe/2.0.0: resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} - dev: true /js-stringify/1.0.2: resolution: {integrity: sha1-Fzb939lyTyijaCrcYjCufk6Weds=} @@ -1956,6 +2168,32 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: false + /make-fetch-happen/9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + dependencies: + agentkeepalive: 4.2.1 + cacache: 15.3.0 + http-cache-semantics: 4.1.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.0 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.1.3 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.2 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + optional: true + /marked/2.0.7: resolution: {integrity: sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ==} engines: {node: '>= 8.16.2'} @@ -2001,6 +2239,50 @@ packages: resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} dev: false + /minipass-collect/1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.1.3 + dev: false + optional: true + + /minipass-fetch/1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + dependencies: + minipass: 3.1.3 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + dev: false + optional: true + + /minipass-flush/1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.1.3 + dev: false + optional: true + + /minipass-pipeline/1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + dependencies: + minipass: 3.1.3 + dev: false + optional: true + + /minipass-sized/1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + dependencies: + minipass: 3.1.3 + dev: false + optional: true + /minipass/3.1.3: resolution: {integrity: sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==} engines: {node: '>=8'} @@ -2101,6 +2383,10 @@ packages: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} dev: false + /node-addon-api/4.3.0: + resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + dev: false + /node-fetch/2.6.1: resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==} engines: {node: 4.x || >=6.0.0} @@ -2114,6 +2400,28 @@ packages: fetch-blob: 3.1.2 dev: false + /node-gyp/8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + requiresBuild: true + dependencies: + env-paths: 2.2.1 + glob: 7.1.7 + graceful-fs: 4.2.6 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.3.5 + tar: 6.1.11 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + optional: true + /nopt/5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -2136,8 +2444,19 @@ packages: set-blocking: 2.0.0 dev: false + /npmlog/6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + dev: false + optional: true + /number-is-nan/1.0.1: - resolution: {integrity: sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=} + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} engines: {node: '>=0.10.0'} dev: false @@ -2215,6 +2534,14 @@ packages: engines: {node: '>=6'} dev: false + /p-map/4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: false + optional: true + /packet-reader/1.0.0: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} dev: false @@ -2360,6 +2687,25 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + /promise-inflight/1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dev: false + optional: true + + /promise-retry/2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + dev: false + optional: true + /promise/7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} dependencies: @@ -2566,6 +2912,12 @@ packages: signal-exit: 3.0.3 dev: false + /retry/0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: false + optional: true + /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2630,7 +2982,7 @@ packages: dev: true /set-blocking/2.0.0: - resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=} + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false /shebang-command/2.0.0: @@ -2666,6 +3018,11 @@ packages: resolution: {integrity: sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==} dev: false + /signal-exit/3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + optional: true + /sinon/12.0.1: resolution: {integrity: sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==} dependencies: @@ -2691,6 +3048,33 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true + /smart-buffer/4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + optional: true + + /socks-proxy-agent/6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + socks: 2.7.0 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + + /socks/2.7.0: + resolution: {integrity: sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==} + engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + dependencies: + ip: 2.0.0 + smart-buffer: 4.2.0 + dev: false + optional: true + /source-map-support/0.5.19: resolution: {integrity: sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==} dependencies: @@ -2713,6 +3097,27 @@ packages: resolution: {integrity: sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=} dev: true + /sqlite/4.1.2: + resolution: {integrity: sha512-FlBG51gHbux5vPjwnoqFEghNGvnTMTbHyiI09U3qFTQs9AtWuwd4i++6+WCusCXKrVdIDLzfdGekrolr3m4U4A==} + dev: false + + /sqlite3/5.1.1: + resolution: {integrity: sha512-mMinkrQr/LKJqFiFF+AF7imPSzRCCpTCreusZO3D/ssJHVjZOrbu2Caz+zPH5KTmGGXBxXMGSRDssL+44CLxvg==} + requiresBuild: true + peerDependenciesMeta: + node-gyp: + optional: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.5 + node-addon-api: 4.3.0 + tar: 6.1.11 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + /ssh2/1.1.0: resolution: {integrity: sha512-CidQLG2ZacoT0Z7O6dOyisj4JdrOrLVJ4KbHjVNz9yI1vO08FAYQPcnkXY9BP8zeYo+J/nBgY6Gg4R7w4WFWtg==} engines: {node: '>=10.16.0'} @@ -2725,6 +3130,14 @@ packages: nan: 2.16.0 dev: false + /ssri/8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.1.3 + dev: false + optional: true + /standard-as-callback/2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} dev: false @@ -2735,7 +3148,7 @@ packages: dev: false /string-width/1.0.2: - resolution: {integrity: sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=} + resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} engines: {node: '>=0.10.0'} dependencies: code-point-at: 1.1.0 @@ -2751,6 +3164,16 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.0 + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + optional: true + /string_decoder/1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -2764,7 +3187,7 @@ packages: dev: false /strip-ansi/3.0.1: - resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=} + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} engines: {node: '>=0.10.0'} dependencies: ansi-regex: 2.1.1 @@ -2776,6 +3199,14 @@ packages: dependencies: ansi-regex: 5.0.0 + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + optional: true + /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2821,8 +3252,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tar/6.1.0: - resolution: {integrity: sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==} + /tar/6.1.11: + resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} engines: {node: '>= 10'} dependencies: chownr: 2.0.0 @@ -3032,6 +3463,20 @@ packages: dev: false optional: true + /unique-filename/1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + dependencies: + unique-slug: 2.0.2 + dev: false + optional: true + + /unique-slug/2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + dependencies: + imurmurhash: 0.1.4 + dev: false + optional: true + /universalify/2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -3082,7 +3527,6 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: true /wide-align/1.1.3: resolution: {integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==} @@ -3090,6 +3534,13 @@ packages: string-width: 1.0.2 dev: false + /wide-align/1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + optional: true + /with/7.0.2: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} diff --git a/src/migrations/2022-04-28T19:04:55.000Z_CreateOAuth2TokensTable.migration.ts b/src/migrations/2022-04-28T19:04:55.000Z_CreateOAuth2TokensTable.migration.ts index e03a2c5..a7fa18d 100644 --- a/src/migrations/2022-04-28T19:04:55.000Z_CreateOAuth2TokensTable.migration.ts +++ b/src/migrations/2022-04-28T19:04:55.000Z_CreateOAuth2TokensTable.migration.ts @@ -1,4 +1,4 @@ -import {DatabaseService, FieldType, Migration, raw, Schema} from '../orm' +import {DatabaseService, FieldType, Migration, Schema} from '../orm' import {Inject} from '../di' export default class CreateOAuth2TokensTableMigration extends Migration { @@ -6,8 +6,8 @@ export default class CreateOAuth2TokensTableMigration extends Migration { protected readonly db!: DatabaseService async up(): Promise { - const schema: Schema = this.db.get().schema() - const table = await schema.table('oauth2_tokens') + const db = this.db.get() + const table = await db.schema().table('oauth2_tokens') table.primaryKey('oauth2_token_id').required() @@ -21,7 +21,7 @@ export default class CreateOAuth2TokensTableMigration extends Migration { table.column('issued') .type(FieldType.timestamp) - .default(raw('NOW()')) + .default(db.dialect().currentTimestamp()) .required() table.column('expires') @@ -32,7 +32,7 @@ export default class CreateOAuth2TokensTableMigration extends Migration { .type(FieldType.varchar) .nullable() - await schema.commit(table) + await db.schema().commit(table) } async down(): Promise { diff --git a/src/orm/builder/AbstractBuilder.ts b/src/orm/builder/AbstractBuilder.ts index 331d491..bc11464 100644 --- a/src/orm/builder/AbstractBuilder.ts +++ b/src/orm/builder/AbstractBuilder.ts @@ -144,7 +144,7 @@ export abstract class AbstractBuilder extends AppClass { * @param table * @param alias */ - from(table: string, alias?: string): this { + from(table: string|QuerySafeValue, alias?: string): this { if ( alias ) { this.source = { table, alias } diff --git a/src/orm/connection/Connection.ts b/src/orm/connection/Connection.ts index 209932d..b4703dd 100644 --- a/src/orm/connection/Connection.ts +++ b/src/orm/connection/Connection.ts @@ -1,11 +1,12 @@ -import {Awaitable, ErrorWithContext} from '../../util' -import {QueryResult} from '../types' +import {Awaitable, Collection, ErrorWithContext} from '../../util' +import {QueryResult, QueryRow} from '../types' import {SQLDialect} from '../dialect/SQLDialect' import {AppClass} from '../../lifecycle/AppClass' import {Inject, Injectable} from '../../di' import {QueryExecutedEvent} from './event/QueryExecutedEvent' import {Schema} from '../schema/Schema' import {Bus} from '../../support/bus' +import {ModelField} from '../model/Field' /** * Error thrown when a connection is used before it is ready. @@ -75,6 +76,17 @@ export abstract class Connection extends AppClass { */ public abstract asTransaction(closure: () => Awaitable): Awaitable + /** + * Normalize a query row before it is used by the framework. + * This helps account for differences in return values from the dialects. + * @param row + * @param fields + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public normalizeRow(row: QueryRow, fields: Collection): QueryRow { + return row + } + /** * Fire a QueryExecutedEvent for the given query string. * @param query diff --git a/src/orm/connection/PostgresConnection.ts b/src/orm/connection/PostgresConnection.ts index fa7fefd..86de20d 100644 --- a/src/orm/connection/PostgresConnection.ts +++ b/src/orm/connection/PostgresConnection.ts @@ -80,9 +80,21 @@ export class PostgresConnection extends Connection { } await this.client.query('BEGIN') - const result = await closure() - await this.client.query('COMMIT') - return result + try { + const result = await closure() + await this.client.query('COMMIT') + return result + } catch (e) { + await this.client.query('ROLLBACK') + + if ( e instanceof Error ) { + throw this.app().errorWrapContext(e, { + connection: this.name, + }) + } + + throw e + } } public schema(name?: string): Schema { diff --git a/src/orm/connection/SQLiteConnection.ts b/src/orm/connection/SQLiteConnection.ts new file mode 100644 index 0000000..64b0aed --- /dev/null +++ b/src/orm/connection/SQLiteConnection.ts @@ -0,0 +1,117 @@ +import {Connection, ConnectionNotReadyError} from './Connection' +import {Logging} from '../../service/Logging' +import {Inject} from '../../di' +import {open, Database} from 'sqlite' +import {FieldType, QueryResult, QueryRow} from '../types' +import {Schema} from '../schema/Schema' +import {Awaitable, collect, Collection, hasOwnProperty, UniversalPath} from '../../util' +import {SQLDialect} from '../dialect/SQLDialect' +import {SQLiteDialect} from '../dialect/SQLiteDialect' +import {SQLiteSchema} from '../schema/SQLiteSchema' +import * as sqlite3 from 'sqlite3' +import {ModelField} from '../model/Field' + +export interface SQLiteConnectionConfig { + filename: string, +} + +export class SQLiteConnection extends Connection { + @Inject() + protected readonly logging!: Logging + + protected client?: Database + + public dialect(): SQLDialect { + return this.make(SQLiteDialect) + } + + public async init(): Promise { + if ( this.config?.filename instanceof UniversalPath ) { + this.config.filename = this.config.filename.toLocal + } + + this.logging.debug(`Opening SQLite connection ${this.name} (${this.config?.filename})...`) + + this.client = await open({ + ...this.config, + driver: sqlite3.Database, + }) + } + + public async close(): Promise { + this.logging.debug(`Closing SQLite connection ${this.name}...`) + if ( this.client ) { + await this.client.close() + } + } + + public async query(query: string): Promise { + if ( !this.client ) { + throw new ConnectionNotReadyError(this.name, { + config: this.config, + }) + } + + this.logging.verbose(`Executing query in connection ${this.name}: \n${query.split('\n').map(x => ' ' + x) + .join('\n')}`) + + try { + const result = await this.client.all(query) // FIXME: this probably won't work for non-select statements? + await this.queryExecuted(query) + + return { + rows: collect(result), + rowCount: result.length, + } + } catch (e) { + if ( e instanceof Error ) { + throw this.app().errorWrapContext(e, { + query, + connection: this.name, + }) + } + + throw e + } + } + + public async asTransaction(closure: () => Awaitable): Promise { + if ( !this.client ) { + throw new ConnectionNotReadyError(this.name, { + config: this.config, + }) + } + + // fixme: sqlite doesn't support tx's properly in node atm + await this.client.run('BEGIN') + try { + const result = await closure() + await this.client.run('COMMIT') + return result + } catch (e) { + await this.client.run('ROLLBACK') + + if ( e instanceof Error ) { + throw this.app().errorWrapContext(e, { + connection: this.name, + }) + } + + throw e + } + } + + public normalizeRow(row: QueryRow, fields: Collection): QueryRow { + fields.where('type', '=', FieldType.json) + .pluck('databaseKey') + .filter(key => hasOwnProperty(row, key)) + .filter(key => typeof row[key] === 'string') + .each(key => row[key] = JSON.parse(row[key])) + + return row + } + + public schema(): Schema { + return new SQLiteSchema(this) + } +} diff --git a/src/orm/dialect/PostgreSQLDialect.ts b/src/orm/dialect/PostgreSQLDialect.ts index a4b4b85..e9adfed 100644 --- a/src/orm/dialect/PostgreSQLDialect.ts +++ b/src/orm/dialect/PostgreSQLDialect.ts @@ -1,5 +1,5 @@ import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect' -import {Constraint, inverseFieldType, isConstraintGroup, isConstraintItem, SpecifiedField} from '../types' +import {Constraint, inverseFieldType, isConstraintGroup, isConstraintItem, QuerySource, SpecifiedField} from '../types' import {AbstractBuilder} from '../builder/AbstractBuilder' import {ColumnBuilder, ConstraintBuilder, ConstraintType, IndexBuilder, TableBuilder} from '../schema/TableBuilder' import {collect, Collectable, Collection, ErrorWithContext, hasOwnProperty, Maybe} from '../../util' @@ -44,6 +44,19 @@ export class PostgreSQLDialect extends SQLDialect { } } + public renderQuerySource(source: QuerySource): string { + if ( source instanceof QuerySafeValue ) { + return String(source) + } else if ( typeof source === 'string' ) { + return source.replace(/"/g, '""') + .split('.') + .map(x => '"' + x + '"') + .join('.') + } + + return `${this.renderQuerySource(source.table)} AS "${source.alias.replace(/"/g, '""')}"` + } + public renderCount(query: string): string { return [ 'SELECT COUNT(*) AS "extollo_render_count"', @@ -59,7 +72,7 @@ export class PostgreSQLDialect extends SQLDialect { 'FROM (', ...query.split('\n').map(x => ` ${x}`), ') AS extollo_target_query', - `OFFSET ${start} LIMIT ${(end - start) + 1}`, // FIXME - the +1 is only needed when start === end + `OFFSET ${start} LIMIT ${start === end ? ((end - start) + 1) : (end - start)}`, ].join('\n') } @@ -111,10 +124,7 @@ export class PostgreSQLDialect extends SQLDialect { // FIXME error if no source const source = builder.querySource if ( source ) { - const tableString = typeof source === 'string' ? source : source.table - const table: string = tableString.split('.').map(x => `"${x}"`) - .join('.') - queryLines.push('FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`)) + queryLines.push('FROM ' + this.renderQuerySource(source)) } // Add constraints @@ -164,22 +174,20 @@ export class PostgreSQLDialect extends SQLDialect { const queryLines: string[] = [] // Add table source - const source = builder.querySource - let sourceAlias = 'extollo_update_source' + let source = builder.querySource if ( !source ) { throw new ErrorWithContext('No table specified for update query') } - const tableString = typeof source === 'string' ? source : source.table - const table: string = tableString.split('.') - .map(x => `"${x}"`) - .join('.') - - if ( typeof source !== 'string' && source.alias ) { - sourceAlias = source.alias + source = (typeof source !== 'string' && !(source instanceof QuerySafeValue)) ? source : { + table: source, + alias: 'extollo_update_source', } - queryLines.push('UPDATE ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`)) + const sourceAlias = source.alias + const sourceTable = source.table + + queryLines.push('UPDATE ' + this.renderQuerySource(source)) queryLines.push('SET') const updateFields = this.getAllFieldsFromUpdateRows(rows) @@ -192,7 +200,7 @@ export class PostgreSQLDialect extends SQLDialect { // FIXME: This is fairly inefficient. Probably a better way with a FROM ... SELECT // return raw(`"${sourceAlias}"."${field}"`) - return raw(`(SELECT "${field}" FROM ${table} WHERE "${primaryKey}" = ${this.escape(row[primaryKey])})`) + return raw(`(SELECT "${field}" FROM ${sourceTable} WHERE "${primaryKey}" = ${this.escape(row[primaryKey])})`) }) }) @@ -238,10 +246,7 @@ export class PostgreSQLDialect extends SQLDialect { // Add table source const source = builder.querySource if ( source ) { - const tableString = typeof source === 'string' ? source : source.table - const table: string = tableString.split('.').map(x => `"${x}"`) - .join('.') - queryLines.push('UPDATE ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`)) + queryLines.push('UPDATE ' + this.renderQuerySource(source)) } queryLines.push(this.renderUpdateSet(data)) @@ -306,10 +311,7 @@ export class PostgreSQLDialect extends SQLDialect { // Add table source const source = builder.querySource if ( source ) { - const tableString = typeof source === 'string' ? source : source.table - const table: string = tableString.split('.').map(x => `"${x}"`) - .join('.') - queryLines.push('INSERT INTO ' + (typeof source === 'string' ? table : `${table} AS "${source.alias}"`) + queryLines.push('INSERT INTO ' + this.renderQuerySource(source) + (columns.length ? ` (${columns.map(x => `"${x}"`).join(', ')})` : '')) } @@ -352,10 +354,7 @@ export class PostgreSQLDialect extends SQLDialect { // Add table source const source = builder.querySource if ( source ) { - const tableString = typeof source === 'string' ? source : source.table - const table: string = tableString.split('.').map(x => `"${x}"`) - .join('.') - queryLines.push('DELETE FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`)) + queryLines.push('DELETE FROM ' + this.renderQuerySource(source)) } // Add constraints @@ -662,4 +661,8 @@ export class PostgreSQLDialect extends SQLDialect { return parts.join('\n') } + + public currentTimestamp(): QuerySafeValue { + return raw('NOW()') + } } diff --git a/src/orm/dialect/SQLDialect.ts b/src/orm/dialect/SQLDialect.ts index 8762b35..244b5a3 100644 --- a/src/orm/dialect/SQLDialect.ts +++ b/src/orm/dialect/SQLDialect.ts @@ -1,4 +1,4 @@ -import {Constraint} from '../types' +import {Constraint, QuerySource} from '../types' import {AbstractBuilder} from '../builder/AbstractBuilder' import {AppClass} from '../../lifecycle/AppClass' import {ColumnBuilder, IndexBuilder, TableBuilder} from '../schema/TableBuilder' @@ -55,6 +55,12 @@ export abstract class SQLDialect extends AppClass { */ public abstract escape(value: EscapeValue): QuerySafeValue + /** + * Render a query source object as a qualified table name string ("tablename" as "alias"). + * @param source + */ + public abstract renderQuerySource(source: QuerySource): string; + /** * Render the given query builder as a "SELECT ..." query string. * @@ -261,6 +267,12 @@ export abstract class SQLDialect extends AppClass { */ public abstract renderTransaction(queries: string[]): string; + /** + * Get the expression for the current timestamp as an escaped value. + * @example `raw('NOW()')` + */ + public abstract currentTimestamp(): QuerySafeValue; + /** * Given a table schema-builder, render a series of queries as a transaction * that apply the given schema to database. diff --git a/src/orm/dialect/SQLiteDialect.ts b/src/orm/dialect/SQLiteDialect.ts new file mode 100644 index 0000000..41144f6 --- /dev/null +++ b/src/orm/dialect/SQLiteDialect.ts @@ -0,0 +1,617 @@ +import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect' +import {Collection, ErrorWithContext, Maybe} from '../../util' +import {AbstractBuilder} from '../builder/AbstractBuilder' +import { + Constraint, + FieldType, + inverseFieldType, + isConstraintGroup, + isConstraintItem, + QuerySource, + SpecifiedField, +} from '../types' +import {ColumnBuilder, ConstraintBuilder, ConstraintType, IndexBuilder, TableBuilder} from '../schema/TableBuilder' + +export class SQLiteDialect extends SQLDialect { + public escape(value: EscapeValue): QuerySafeValue { + if ( value instanceof QuerySafeValue ) { + return value + } else if ( Array.isArray(value) || value instanceof Collection ) { + return new QuerySafeValue(value, `(${value.map(v => this.escape(v)).join(',')})`) + } else if ( String(value).toLowerCase() === 'true' || value === true ) { + return new QuerySafeValue(value, 'TRUE') + } else if ( String(value).toLowerCase() === 'false' || value === false ) { + return new QuerySafeValue(value, 'FALSE') + } else if ( typeof value === 'number' ) { + return new QuerySafeValue(value, `${value}`) + } else if ( value instanceof Date ) { + const pad = (val: number) => val < 10 ? `0${val}` : `${val}` + const [y, m, d, h, i, s] = [ + `${value.getFullYear()}`, + `${pad(value.getMonth() + 1)}`, + `${pad(value.getDate())}`, + `${pad(value.getHours())}`, + `${pad(value.getMinutes())}`, + `${pad(value.getSeconds())}`, + ] + + return new QuerySafeValue(value, `'${y}-${m}-${d} ${h}:${i}:${s}'`) + } else if ( value === null || typeof value === 'undefined' ) { + return new QuerySafeValue(value, 'NULL') + } else if ( !isNaN(Number(value)) ) { + return new QuerySafeValue(value, String(Number(value))) + } else { + const escaped = value.replace(/'/g, '\'\'') + return new QuerySafeValue(value, `'${escaped}'`) + } + } + + public renderQuerySource(source: QuerySource): string { + if ( source instanceof QuerySafeValue ) { + return String(source) + } else if ( typeof source === 'string' ) { + return source.replace(/"/g, '""') + .split('.') + .map(x => '"' + x + '"') + .join('.') + } + + return `${this.renderQuerySource(source.table)} AS "${source.alias.replace(/"/g, '""')}"` + } + + public renderCount(query: string): string { + return [ + 'SELECT COUNT(*) AS "extollo_render_count"', + 'FROM (', + ...query.split('\n').map(x => ` ${x}`), + ') AS extollo_target_query', + ].join('\n') + } + + public renderRangedSelect(query: string, start: number, end: number): string { + return [ + 'SELECT *', + 'FROM (', + ...query.split('\n').map(x => ` ${x}`), + ') AS extollo_target_query', + `LIMIT ${start === end ? ((end - start) + 1) : (end - start)} OFFSET ${start}`, + ].join('\n') + } + + /** Render the fields from the builder class to PostgreSQL syntax. */ + protected renderFields(builder: AbstractBuilder): string[] { + return builder.appliedFields.map((field: SpecifiedField) => { + let columnString: string + if ( typeof field === 'string' ) { + columnString = field.split('.').map(x => `"${x}"`) + .join('.') + } else if ( field instanceof QuerySafeValue ) { + columnString = field.toString() + } else if ( typeof field.field === 'string' ) { + columnString = field.field.split('.').map(x => `"${x}"`) + .join('.') + } else { + columnString = field.field.toString() + } + + let aliasString = '' + if ( typeof field !== 'string' && !(field instanceof QuerySafeValue) ) { + aliasString = ` AS "${field.alias}"` + } + + return `${columnString}${aliasString}` + }) + } + + public renderSelect(builder: AbstractBuilder): string { + const rawSql = builder.appliedRawSql + if ( rawSql ) { + return rawSql + } + + const indent = (item: string, level = 1) => Array(level + 1).fill('') + .join(' ') + item + const queryLines = [ + `SELECT${builder.appliedDistinction ? ' DISTINCT' : ''}`, + ] + + // Add fields + // FIXME error if no fields + const fields = this.renderFields(builder).map(x => indent(x)) + .join(',\n') + + queryLines.push(fields) + + // Add table source + // FIXME error if no source + const source = builder.querySource + if ( source ) { + queryLines.push('FROM ' + this.renderQuerySource(source)) + } + + // Add constraints + const wheres = this.renderConstraints(builder.appliedConstraints) + if ( wheres.trim() ) { + queryLines.push('WHERE') + queryLines.push(wheres) + } + + // Add group by + if ( builder.appliedGroupings?.length ) { + const grouping = builder.appliedGroupings.map(group => { + return indent(group.split('.').map(x => `"${x}"`) + .join('.')) + }).join(',\n') + + queryLines.push('GROUP BY') + queryLines.push(grouping) + } + + // Add order by + if ( builder.appliedOrder?.length ) { + const ordering = builder.appliedOrder.map(x => indent(`${x.field.split('.').map(y => '"' + y + '"') + .join('.')} ${x.direction}`)).join(',\n') + queryLines.push('ORDER BY') + queryLines.push(ordering) + } + + // Add limit/offset + const pagination = builder.appliedPagination + if ( pagination.take ) { + queryLines.push(`LIMIT ${pagination.take}${pagination.skip ? ' OFFSET ' + pagination.skip : ''}`) + } else if ( pagination.skip ) { + queryLines.push(`LIMIT -1 OFFSET ${pagination.skip}`) + } + + return queryLines.join('\n') + } + + public renderBatchUpdate(): string { + throw new ErrorWithContext('SQLite dialect does not support batch updates.') + } + + // TODO support FROM, RETURNING + public renderUpdate(builder: AbstractBuilder, data: {[key: string]: EscapeValue}): string { + const rawSql = builder.appliedRawSql + if ( rawSql ) { + return rawSql + } + + const queryLines: string[] = [] + + // Add table source + const source = builder.querySource + if ( source ) { + queryLines.push('UPDATE ' + this.renderQuerySource(source)) + } + + queryLines.push(this.renderUpdateSet(data)) + + // Add constraints + const wheres = this.renderConstraints(builder.appliedConstraints) + if ( wheres.trim() ) { + queryLines.push('WHERE') + queryLines.push(wheres) + } + + const fields = this.renderFields(builder).map(x => ` ${x}`) + .join(',\n') + + if ( fields ) { + queryLines.push('RETURNING') + queryLines.push(fields) + } + + return queryLines.join('\n') + } + + public renderExistential(builder: AbstractBuilder): string { + const rawSql = builder.appliedRawSql + if ( rawSql ) { + return ` + SELECT EXISTS( + ${rawSql} + ) + ` + } + + const query = builder.clone() + .clearFields() + .field(raw('TRUE')) + .limit(1) + + return this.renderSelect(query) + } + + // FIXME: subquery support here and with select + public renderInsert(builder: AbstractBuilder, data: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[] = []): string { + const rawSql = builder.appliedRawSql + if ( rawSql ) { + return rawSql + } + + const indent = (item: string, level = 1) => Array(level + 1).fill('') + .join(' ') + item + const queryLines: string[] = [] + + if ( !Array.isArray(data) ) { + data = [data] + } + + if ( data.length < 1 ) { + return '' + } + + const columns = Object.keys(data[0]) + + // Add table source + const source = builder.querySource + if ( source ) { + queryLines.push('INSERT INTO ' + this.renderQuerySource(source) + + (columns.length ? ` (${columns.map(x => `"${x}"`).join(', ')})` : '')) + } + + if ( Array.isArray(data) && !data.length ) { + queryLines.push('DEFAULT VALUES') + } else { + queryLines.push('VALUES') + + const valueString = data.map(row => { + const values = columns.map(x => this.escape(row[x])) + return indent(`(${values.join(', ')})`) + }) + .join(',\n') + + queryLines.push(valueString) + } + + // Add return fields + if ( builder.appliedFields?.length ) { + queryLines.push('RETURNING') + const fields = this.renderFields(builder).map(x => indent(x)) + .join(',\n') + + queryLines.push(fields) + } + + return queryLines.join('\n') + } + + public renderDelete(builder: AbstractBuilder): string { + const rawSql = builder.appliedRawSql + if ( rawSql ) { + return rawSql + } + + const indent = (item: string, level = 1) => Array(level + 1).fill('') + .join(' ') + item + const queryLines: string[] = [] + + // Add table source + const source = builder.querySource + if ( source ) { + queryLines.push('DELETE FROM ' + this.renderQuerySource(source)) + } + + // Add constraints + const wheres = this.renderConstraints(builder.appliedConstraints) + if ( wheres.trim() ) { + queryLines.push('WHERE') + queryLines.push(wheres) + } + + // Add return fields + if ( builder.appliedFields?.length ) { + queryLines.push('RETURNING') + + const fields = this.renderFields(builder).map(x => indent(x)) + .join(',\n') + + queryLines.push(fields) + } + + return queryLines.join('\n') + } + + public renderConstraints(allConstraints: Constraint[], startingLevel = 1): string { + const constraintsToSql = (constraints: Constraint[], level = startingLevel): string => { + const indent = Array(level * 2).fill(' ') + .join('') + const statements = [] + + for ( const constraint of constraints ) { + if ( isConstraintGroup(constraint) ) { + statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}(\n${constraintsToSql(constraint.items, level + 1)}\n${indent})`) + } else if ( isConstraintItem(constraint) ) { + if ( Array.isArray(constraint.operand) && !constraint.operand.length ) { + statements.push(`${indent}1 = 0 -- ${constraint.field} ${constraint.operator} empty set`) + continue + } + + const field: string = constraint.field.split('.').map(x => `"${x}"`) + .join('.') + statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}${field} ${constraint.operator} ${this.escape(constraint.operand).value}`) + } else if ( constraint instanceof QuerySafeValue ) { + statements.push(`${indent}${statements.length < 1 ? '' : 'AND '}${constraint.toString()}`) + } + } + + return statements.filter(Boolean).join('\n') + } + + return constraintsToSql(allConstraints) + } + + public renderUpdateSet(data: {[key: string]: EscapeValue}): string { + const sets = [] + for ( const key in data ) { + if ( !Object.prototype.hasOwnProperty.call(data, key) ) { + continue + } + + sets.push(` "${key}" = ${this.escape(data[key])}`) + } + + return `SET\n${sets.join(',\n')}` + } + + public renderCreateTable(builder: TableBuilder): string { + const cols = this.renderTableColumns(builder).map(x => ` ${x}`) + + const builderConstraints = builder.getConstraints() + const constraints: string[] = [] + for ( const constraintName in builderConstraints ) { + if ( !Object.prototype.hasOwnProperty.call(builderConstraints, constraintName) ) { + continue + } + + const constraintBuilder = builderConstraints[constraintName] + const constraintDefinition = this.renderConstraintDefinition(constraintBuilder) + if ( constraintDefinition ) { + constraints.push(` CONSTRAINT ${constraintDefinition}`) + } + } + + const parts = [ + `CREATE TABLE ${builder.isSkippedIfExisting() ? 'IF NOT EXISTS ' : ''}${builder.name} (`, + [ + ...cols, + ...constraints, + ].join(',\n'), + `)`, + ] + + return parts.join('\n') + } + + public renderTableColumns(builder: TableBuilder): string[] { + const defined = builder.getColumns() + const rendered: string[] = [] + + for ( const columnName in defined ) { + if ( !Object.prototype.hasOwnProperty.call(defined, columnName) ) { + continue + } + + const columnBuilder = defined[columnName] + rendered.push(this.renderColumnDefinition(columnBuilder)) + } + + return rendered + } + + /** + * Given a constraint schema-builder, render the constraint definition. + * @param builder + * @protected + */ + protected renderConstraintDefinition(builder: ConstraintBuilder): Maybe { + const constraintType = builder.getType() + if ( constraintType === ConstraintType.Unique ) { + const fields = builder.getFields() + .map(x => `"${x}"`) + .join(',') + + return `${builder.name} UNIQUE(${fields})` + } else if ( constraintType === ConstraintType.Check ) { + const expression = builder.getExpression() + if ( !expression ) { + throw new ErrorWithContext('Cannot create check constraint without expression.', { + constraintName: builder.name, + tableName: builder.parent.name, + }) + } + + return `${builder.name} CHECK(${expression})` + } + } + + /** + * Given a column-builder, render the SQL-definition as used in + * CREATE TABLE and ALTER TABLE statements. + * @fixme Type `serial` only exists on CREATE TABLE... queries + * @param builder + * @protected + */ + protected renderColumnDefinition(builder: ColumnBuilder): string { + const type = builder.getType() + if ( !type ) { + throw new ErrorWithContext(`Missing field type for column: ${builder.name}`, { + columnName: builder.name, + columnType: type, + }) + } + + let render = `${builder.name} ${inverseFieldType(this.mapFieldType(type))}` + + if ( builder.getLength() ) { + render += `(${builder.getLength()})` + } + + const defaultValue = builder.getDefaultValue() + if ( typeof defaultValue !== 'undefined' ) { + render += ` DEFAULT ${this.escape(defaultValue)}` + } + + if ( builder.isPrimary() ) { + render += ` PRIMARY KEY` + } + + if ( type === FieldType.serial ) { + render += ` AUTOINCREMENT` + } + + if ( builder.isUnique() ) { + render += ` UNIQUE` + } + + render += ` ${builder.isNullable() ? 'NULL' : 'NOT NULL'}` + return render + } + + private mapFieldType(type: FieldType): FieldType { + if ( type === FieldType.serial ) { + return FieldType.integer + } + + return type + } + + public renderDropTable(builder: TableBuilder): string { + return `DROP TABLE ${builder.isSkippedIfExisting() ? 'IF EXISTS ' : ''}${builder.name}` + } + + public renderCreateIndex(builder: IndexBuilder): string { + const cols = builder.getFields().map(x => `"${x}"`) + const parts = [ + `CREATE ${builder.isUnique() ? 'UNIQUE ' : ''}INDEX ${builder.isSkippedIfExisting() ? 'IF NOT EXISTS ' : ''}${builder.name}`, + ` ON ${builder.parent.name}`, + ` (${cols.join(',')})`, + ] + + return parts.join('\n') + } + + public renderAlterTable(builder: TableBuilder): string { + const alters: string[] = [] + const columns = builder.getColumns() + + for ( const columnName in columns ) { + if ( !Object.prototype.hasOwnProperty.call(columns, columnName) ) { + continue + } + + const columnBuilder = columns[columnName] + if ( !columnBuilder.isExisting() ) { + // The column doesn't exist on the table, but was added to the schema + alters.push(` ADD COLUMN ${this.renderColumnDefinition(columnBuilder)}`) + } else if ( columnBuilder.isDirty() && columnBuilder.originalFromSchema ) { + // The column exists in the table, but was modified in the schema + if ( columnBuilder.isDropping() || columnBuilder.isDroppingIfExists() ) { + alters.push(` DROP COLUMN "${columnBuilder.name}"`) + continue + } + + // Change the data type of the column + if ( columnBuilder.getType() !== columnBuilder.originalFromSchema.getType() ) { + const renderedType = `${columnBuilder.getType()}${columnBuilder.getLength() ? `(${columnBuilder.getLength()})` : ''}` + alters.push(` ALTER COLUMN "${columnBuilder.name}" TYPE ${renderedType}`) + } + + // Change the default value of the column + if ( columnBuilder.getDefaultValue() !== columnBuilder.originalFromSchema.getDefaultValue() ) { + alters.push(` ALTER COLUMN "${columnBuilder.name}" SET default ${this.escape(columnBuilder.getDefaultValue())}`) + } + + // Change the nullable-status of the column + if ( columnBuilder.isNullable() !== columnBuilder.originalFromSchema.isNullable() ) { + if ( columnBuilder.isNullable() ) { + alters.push(` ALTER COLUMN "${columnBuilder.name}" DROP NOT NULL`) + } else { + alters.push(` ALTER COLUMN "${columnBuilder.name}" SET NOT NULL`) + } + } + + // Change the name of the column + if ( columnBuilder.getRename() ) { + alters.push(` RENAME COLUMN "${columnBuilder.name}" TO "${columnBuilder.getRename()}"`) + } + } + } + + const constraints = builder.getConstraints() + for ( const constraintName in constraints ) { + if ( !Object.prototype.hasOwnProperty.call(constraints, constraintName) ) { + continue + } + + const constraintBuilder = constraints[constraintName] + + // Drop the constraint if specified + if ( constraintBuilder.isDropping() ) { + alters.push(` DROP CONSTRAINT ${constraintBuilder.name}`) + continue + } + + // Drop the constraint with IF EXISTS if specified + if ( constraintBuilder.isDroppingIfExists() ) { + alters.push(` DROP CONSTRAINT IF EXISTS ${constraintBuilder.name}`) + continue + } + + // Otherwise, drop and recreate the constraint if it was modified + if ( constraintBuilder.isDirty() ) { + if ( constraintBuilder.isExisting() ) { + alters.push(` DROP CONSTRAINT IF EXISTS ${constraintBuilder.name}`) + } + + const constraintDefinition = this.renderConstraintDefinition(constraintBuilder) + if ( constraintDefinition ) { + alters.push(` ADD CONSTRAINT ${constraintDefinition}`) + } + } + } + + if ( builder.getRename() ) { + alters.push(` RENAME TO "${builder.getRename()}"`) + } + + return 'ALTER TABLE ' + builder.name + '\n' + alters.join(',\n') + } + + public renderDropIndex(builder: IndexBuilder): string { + return `DROP INDEX ${builder.isDroppingIfExists() ? 'IF EXISTS ' : ''}${builder.name}` + } + + public renderTransaction(queries: string[]): string { + return queries.join(';\n\n') // fixme: sqlite3 in node doesn't properly support transactions + // const parts = [ + // 'BEGIN', + // ...queries, + // 'COMMIT;', + // ] + // + // return parts.join(';\n\n') + } + + public renderRenameIndex(builder: IndexBuilder): string { + return `ALTER INDEX ${builder.name} RENAME TO ${builder.getRename()}` + } + + public renderRecreateIndex(builder: IndexBuilder): string { + return `${this.renderDropIndex(builder)};\n\n${this.renderCreateIndex(builder)}` + } + + public renderDropColumn(builder: ColumnBuilder): string { + const parts = [ + `ALTER TABLE ${builder.parent.name} ${builder.parent.isSkippedIfExisting() ? 'IF EXISTS ' : ''}`, + ` DROP COLUMN ${builder.isSkippedIfExisting() ? 'IF EXISTS ' : ''}${builder.name}`, + ] + + return parts.join('\n') + } + + public currentTimestamp(): QuerySafeValue { + return raw('CURRENT_TIMESTAMP') + } +} diff --git a/src/orm/index.ts b/src/orm/index.ts index 7e507d8..b9a2ca1 100644 --- a/src/orm/index.ts +++ b/src/orm/index.ts @@ -7,11 +7,13 @@ export * from './builder/Builder' export * from './connection/Connection' export * from './connection/PostgresConnection' +export * from './connection/SQLiteConnection' export * from './connection/event/QueryExecutedEvent' export * from './connection/event/QueryExecutedEventSerializer' export * from './dialect/SQLDialect' export * from './dialect/PostgreSQLDialect' +export * from './dialect/SQLiteDialect' export * from './model/Field' export * from './model/ModelBuilder' @@ -45,6 +47,7 @@ export * from './types' export * from './schema/TableBuilder' export * from './schema/Schema' export * from './schema/PostgresSchema' +export * from './schema/SQLiteSchema' export * from './services/Migrations' export * from './migrations/Migrator' diff --git a/src/orm/model/Model.ts b/src/orm/model/Model.ts index 25ce4ba..842b547 100644 --- a/src/orm/model/Model.ts +++ b/src/orm/model/Model.ts @@ -4,7 +4,7 @@ import {DatabaseService} from '../DatabaseService' import {ModelBuilder} from './ModelBuilder' import {getFieldsMeta, ModelField} from './Field' import {deepCopy, Collection, uuid4, isKeyof, Pipeline, hasOwnProperty} from '../../util' -import {EscapeValueObject} from '../dialect/SQLDialect' +import {EscapeValueObject, QuerySafeValue} from '../dialect/SQLDialect' import {Logging} from '../../service/Logging' import {Connection} from '../connection/Connection' import {ModelRetrievedEvent} from './events/ModelRetrievedEvent' @@ -162,7 +162,7 @@ export abstract class Model> extends LocalBus> builder.connection(this.getConnection()) - if ( typeof source === 'string' ) { + if ( typeof source === 'string' || source instanceof QuerySafeValue ) { builder.from(source) } else { builder.from(source.table, source.alias) @@ -368,7 +368,7 @@ export abstract class Model> extends LocalBus> builder.connection(ModelClass.getConnection()) - if ( typeof source === 'string' ) { + if ( typeof source === 'string' || source instanceof QuerySafeValue ) { builder.from(source) } else { builder.from(source.table, source.alias) diff --git a/src/orm/model/ModelResultIterable.ts b/src/orm/model/ModelResultIterable.ts index 563b5e4..722698f 100644 --- a/src/orm/model/ModelResultIterable.ts +++ b/src/orm/model/ModelResultIterable.ts @@ -2,9 +2,10 @@ import {Model} from './Model' import {AbstractResultIterable} from '../builder/result/AbstractResultIterable' import {Connection} from '../connection/Connection' import {ModelBuilder} from './ModelBuilder' -import {Container, Instantiable} from '../../di' +import {Instantiable} from '../../di' import {QueryRow} from '../types' import {collect, Collection} from '../../util' +import {getFieldsMeta} from './Field' /** * Implementation of the result iterable that returns query results as instances of the defined model. @@ -60,8 +61,11 @@ export class ModelResultIterable> extends AbstractResultItera * @protected */ protected async inflateRow(row: QueryRow): Promise { - return Container.getContainer().make(this.ModelClass) - .assumeFromSource(row) + const model = this.make(this.ModelClass) + const fields = getFieldsMeta(model) + return model.assumeFromSource( + this.connection.normalizeRow(row, fields), + ) } /** diff --git a/src/orm/schema/SQLiteSchema.ts b/src/orm/schema/SQLiteSchema.ts new file mode 100644 index 0000000..2b1fd1c --- /dev/null +++ b/src/orm/schema/SQLiteSchema.ts @@ -0,0 +1,94 @@ +import {Schema} from './Schema' +import {SQLiteConnection} from '../connection/SQLiteConnection' +import {Awaitable} from '../../util' +import {TableBuilder} from './TableBuilder' +import {Builder} from '../builder/Builder' +import {raw} from '../dialect/SQLDialect' + +export class SQLiteSchema extends Schema { + constructor( + connection: SQLiteConnection, + ) { + super(connection) + } + + hasColumn(table: string, name: string): Awaitable { + return (new Builder()).connection(this.connection) + .select(raw('*')) + .from(`pragma_table_info(${this.connection.dialect().escape(table)})`) // FIXME: probably needs to be raw(...) + .where('name', '=', name) + .exists() + } + + hasColumns(table: string, name: string[]): Awaitable { + return (new Builder()).connection(this.connection) + .select(raw('*')) + .from(`pragma_table_info(${this.connection.dialect().escape(table)})`) // FIXME: probably needs to be raw(...) + .whereIn('name', name) + .get() + .count() + .then(num => num === name.length) + } + + hasTable(name: string): Awaitable { + return (new Builder()).connection(this.connection) + .select(raw('*')) + .from('sqlite_master') + .where('type', '=', 'table') + .where('name', '=', name) + .exists() + } + + table(table: string): Awaitable { + return this.populateTable(new TableBuilder(table)) + } + + protected async populateTable(table: TableBuilder): Promise { + if ( !(await this.hasTable(table.name)) ) { + return table + } + + // Load the existing columns + await (new Builder()).connection(this.connection) + .select(raw('*')) + .from(raw(`pragma_table_info(${this.connection.dialect().escape(table.name)})`)) + .get() + .each(col => { + table.column(col.name) + .type(col.type) + .pipe(line => { + return line.unless(col.notnull, builder => builder.nullable()) + .when(col.dflt_value, builder => builder.default(raw(col.dflt_value))) + .when(col.pk, builder => builder.primary()) + }) + .flagAsExistingInSchema() + }) + + // TODO: Load the existing constraints + // TODO: Look up and apply the check constraints + + // Look up table indexes + await (new Builder()).connection(this.connection) + .select(raw('*')) + .from(raw(`pragma_index_list(${this.connection.dialect().escape(table.name)})`)) + .get() + .each(async idx => { + const indexBuilder = table.index(idx.name) + .pipe(line => line.when(idx.unique, builder => builder.unique())) + + const idxColumns = await (new Builder()).connection(this.connection) + .select(raw('*')) + .from(raw(`pragma_index_xinfo(${this.connection.dialect().escape(idx.name)})`)) + .whereNotNull('name') + .get() + .pluck('name') + + idxColumns.whereDefined() + .each(col => indexBuilder.field(col)) + + indexBuilder.flagAsExistingInSchema() + }) + + return table.flagAsExistingInSchema() + } +} diff --git a/src/orm/services/Database.ts b/src/orm/services/Database.ts index 8b0d4f8..5c9e12b 100644 --- a/src/orm/services/Database.ts +++ b/src/orm/services/Database.ts @@ -6,6 +6,7 @@ import {Unit} from '../../lifecycle/Unit' import {Config} from '../../service/Config' import {Logging} from '../../service/Logging' import {MigratorFactory} from '../migrations/MigratorFactory' +import {SQLiteConnection} from '../connection/SQLiteConnection' /** * Application unit responsible for loading and creating database connections from config. @@ -48,6 +49,8 @@ export class Database extends Unit { let conn if ( config?.dialect === 'postgres' ) { conn = this.app().make(PostgresConnection, key, config) + } else if ( config?.dialect === 'sqlite' ) { + conn = this.app().make(SQLiteConnection, key, config) } else { const e = new ErrorWithContext(`Invalid or missing database dialect: ${config.dialect}. Should be one of: postgres`) e.context = { connectionName: key } diff --git a/src/orm/types.ts b/src/orm/types.ts index d872a09..c5974e0 100644 --- a/src/orm/types.ts +++ b/src/orm/types.ts @@ -87,7 +87,7 @@ export type SpecifiedField = string | QuerySafeValue | { field: string | QuerySa /** * Type alias for an item that refers to a table in a database. */ -export type QuerySource = string | { table: string, alias: string } +export type QuerySource = string | QuerySafeValue | { table: string | QuerySafeValue, alias: string } /** * Possible SQL order-by clause directions.