From b991db0307e0ea08b7de29fe884b86018f9284ee Mon Sep 17 00:00:00 2001 From: David Luecke <daff@neyeon.com> Date: Tue, 17 Oct 2023 21:46:33 -0700 Subject: [PATCH] feat(knex): Wings KnexJS adapter (#6) --- generators/adapter/index.tpl.ts | 2 +- generators/adapter/test.tpl.ts | 1 + package-lock.json | 724 ++++++++++++++++++++- packages/adapter-tests/src/declarations.ts | 2 +- packages/knex/LICENSE | 22 + packages/knex/README.md | 23 + packages/knex/package.json | 67 ++ packages/knex/src/error-handler.ts | 98 +++ packages/knex/src/index.ts | 366 +++++++++++ packages/knex/test/connection.ts | 32 + packages/knex/test/error-handler.test.ts | 66 ++ packages/knex/test/index.test.ts | 396 +++++++++++ packages/knex/tsconfig.json | 9 + 13 files changed, 1793 insertions(+), 15 deletions(-) create mode 100644 packages/knex/LICENSE create mode 100644 packages/knex/README.md create mode 100644 packages/knex/package.json create mode 100644 packages/knex/src/error-handler.ts create mode 100644 packages/knex/src/index.ts create mode 100644 packages/knex/test/connection.ts create mode 100644 packages/knex/test/error-handler.test.ts create mode 100644 packages/knex/test/index.test.ts create mode 100644 packages/knex/tsconfig.json diff --git a/generators/adapter/index.tpl.ts b/generators/adapter/index.tpl.ts index 17393ef..f593276 100644 --- a/generators/adapter/index.tpl.ts +++ b/generators/adapter/index.tpl.ts @@ -48,7 +48,7 @@ export class ${uppername}Adapter< async create(data: Data[], params?: Params): Promise<Result[]> async create(data: Data, params?: Params): Promise<Result> - async create(data: Data | Data[], params?: Params): Promise<Result[]> | Promise<Result> { + async create(data: Data | Data[], params?: Params): Promise<Result[] | Result> { if (Array.isArray(data)) { return Promise.all(data.map((current) => this.create(current, params))) } diff --git a/generators/adapter/test.tpl.ts b/generators/adapter/test.tpl.ts index 70d7d78..ebe7476 100644 --- a/generators/adapter/test.tpl.ts +++ b/generators/adapter/test.tpl.ts @@ -10,6 +10,7 @@ import { adapterTests, Person } from '@wingshq/adapter-tests' import { ${uppername}Adapter } from '../src' const testSuite = adapterTests([ + '.id', '.options', '.get', '.get + $select', diff --git a/package-lock.json b/package-lock.json index 4a21cb1..e7a834a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1142,6 +1142,13 @@ "url": "https://github.com/sponsors/daffl" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "optional": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -1589,6 +1596,110 @@ "node": ">=10" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dev": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.0.tgz", @@ -1725,6 +1836,21 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@npmcli/node-gyp": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", @@ -3096,6 +3222,10 @@ "resolved": "packages/adapter-tests", "link": true }, + "node_modules/@wingshq/knex": { + "resolved": "packages/knex", + "link": true + }, "node_modules/@wingshq/memory": { "resolved": "packages/memory", "link": true @@ -3745,6 +3875,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -4222,6 +4361,11 @@ "color-support": "bin.js" } }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, "node_modules/columnify": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", @@ -4251,7 +4395,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, "engines": { "node": ">=14" } @@ -4584,7 +4727,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -4895,6 +5037,15 @@ "node": ">=4" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -5168,7 +5319,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -5342,6 +5492,14 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -5887,6 +6045,14 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-pkg-repo": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz", @@ -5996,6 +6162,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/git-raw-commits": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-3.0.0.tgz", @@ -6253,7 +6424,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "dev": true, "engines": { "node": ">= 0.4.0" } @@ -6515,6 +6685,13 @@ "node": ">=8" } }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "optional": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6654,7 +6831,6 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -7228,6 +7404,83 @@ "node": ">=6" } }, + "node_modules/knex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.0.1.tgz", + "integrity": "sha512-ruASxC6xPyDklRdrcDy6a9iqK+R9cGK214aiQa+D9gX2ZnHZKv6o6JC9ZfgxILxVAul4bZ13c3tgOAHSuQ7/9g==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.1", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/knex/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/knex/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, "node_modules/latest-version": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", @@ -7709,8 +7962,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -8647,8 +8899,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multimatch": { "version": "5.0.0", @@ -9460,6 +9711,15 @@ "node": ">=8.17.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9713,6 +9973,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", + "dev": true + }, "node_modules/pacote": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", @@ -9946,8 +10212,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.10.1", @@ -9989,6 +10254,111 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dev": true, + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pgpass/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -10111,6 +10481,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/preact": { "version": "10.18.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.18.1.tgz", @@ -10856,7 +11265,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -11423,6 +11831,262 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sqlite3": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", + "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^4.2.0", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/sqlite3/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sqlite3/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sqlite3/node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.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.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sqlite3/node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true + }, + "node_modules/sqlite3/node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/sqlite3/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sqlite3/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/sqlite3/node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/ssri": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", @@ -11595,7 +12259,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -11682,6 +12345,14 @@ "node": ">=8" } }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", @@ -11766,6 +12437,14 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -12738,6 +13417,25 @@ "url": "https://github.com/sponsors/daffl" } }, + "packages/knex": { + "name": "@wingshq/knex", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@feathersjs/commons": "^5.0.11", + "@feathersjs/errors": "^5.0.11", + "@wingshq/adapter-commons": "^0.0.0", + "knex": "^3.0.1" + }, + "devDependencies": { + "@wingshq/adapter-tests": "^0.0.0", + "pg": "^8.11.3", + "sqlite3": "^5.1.6" + }, + "engines": { + "node": ">= 20" + } + }, "packages/memory": { "name": "@wingshq/memory", "version": "0.0.0", diff --git a/packages/adapter-tests/src/declarations.ts b/packages/adapter-tests/src/declarations.ts index 4263b78..d7abbe7 100644 --- a/packages/adapter-tests/src/declarations.ts +++ b/packages/adapter-tests/src/declarations.ts @@ -9,7 +9,7 @@ export type AdapterTestName = AdapterBasicTestName | AdapterMethodsTestName | Ad export type Person = { [key: string]: any name: string - age: number + age: number | null created?: boolean } diff --git a/packages/knex/LICENSE b/packages/knex/LICENSE new file mode 100644 index 0000000..920060a --- /dev/null +++ b/packages/knex/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2023 Wings Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + \ No newline at end of file diff --git a/packages/knex/README.md b/packages/knex/README.md new file mode 100644 index 0000000..53f9c3c --- /dev/null +++ b/packages/knex/README.md @@ -0,0 +1,23 @@ +# @wingshq/knex + +[![CI](https://github.com/wingshq/knex/workflows/CI/badge.svg)](https://github.com/wingshq/wings/actions?query=workflow%3ACI) +[![Download Status](https://img.shields.io/npm/dm/@wingshq/knex.svg?style=flat-square)](https://www.npmjs.com/package/@wingshq/knex) +[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/qa8kez8QBx) + +> A Wings adapter for KnexJS + +## Installation + +```bash +$ npm install --save @wingshq/knex +``` + +## Documentation + +See [Wings knex Adapter API documentation](https://wings.codes/adapters/knex.html) for more details. + +## License + +Copyright (c) 2023 [Wings contributors](https://github.com/wingshq/wings/graphs/contributors) + +Licensed under the [MIT license](LICENSE). diff --git a/packages/knex/package.json b/packages/knex/package.json new file mode 100644 index 0000000..798332d --- /dev/null +++ b/packages/knex/package.json @@ -0,0 +1,67 @@ +{ + "name": "@wingshq/knex", + "description": "A Wings adapter for KnexJS", + "version": "0.0.0", + "homepage": "https://wings.codes", + "keywords": [ + "wings", + "wings-adapter" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/wingshq/wings.git", + "directory": "packages/knex" + }, + "author": { + "name": "Wings contributors", + "email": "hello@feathersjs.com", + "url": "https://feathersjs.com" + }, + "contributors": [], + "bugs": { + "url": "https://github.com/wingshq/wings/issues" + }, + "engines": { + "node": ">= 20" + }, + "files": [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "src/**", + "lib/**", + "esm/**" + ], + "module": "./esm/index.js", + "main": "./lib/index.js", + "types": "./src/index.ts", + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./lib/index.js", + "types": "./src/index.ts" + } + }, + "scripts": { + "prepublish": "npm run compile", + "compile:lib": "shx rm -rf lib/ && tsc --module commonjs", + "compile:esm": "shx rm -rf esm/ && tsc --module es2020 --outDir esm", + "compile": "npm run compile:lib && npm run compile:esm", + "test": "npm run compile && shx rm -f db.slite && node --require ts-node/register --test test/**.test.ts" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@wingshq/adapter-commons": "^0.0.0", + "@feathersjs/commons": "^5.0.11", + "@feathersjs/errors": "^5.0.11", + "knex": "^3.0.1" + }, + "devDependencies": { + "@wingshq/adapter-tests": "^0.0.0", + "pg": "^8.11.3", + "sqlite3": "^5.1.6" + } +} diff --git a/packages/knex/src/error-handler.ts b/packages/knex/src/error-handler.ts new file mode 100644 index 0000000..6f4e7dc --- /dev/null +++ b/packages/knex/src/error-handler.ts @@ -0,0 +1,98 @@ +import { errors } from '@feathersjs/errors' + +export const ERROR = Symbol.for('@feathersjs/knex/error') + +export function errorHandler(error: any) { + const { message } = error + let feathersError = error + + if (error.sqlState && error.sqlState.length) { + // remove SQLSTATE marker (#) and pad/truncate SQLSTATE to 5 chars + const sqlState = ('00000' + error.sqlState.replace('#', '')).slice(-5) + + switch (sqlState.slice(0, 2)) { + case '02': + feathersError = new errors.NotFound(message) + break + case '28': + feathersError = new errors.Forbidden(message) + break + case '08': + case '0A': + case '0K': + feathersError = new errors.Unavailable(message) + break + case '20': + case '21': + case '22': + case '23': + case '24': + case '25': + case '40': + case '42': + case '70': + feathersError = new errors.BadRequest(message) + break + default: + feathersError = new errors.GeneralError(message) + } + } else if (error.code === 'SQLITE_ERROR') { + // NOTE (EK): Error codes taken from + // https://www.sqlite.org/c3ref/c_abort.html + switch (error.errno) { + case 1: + case 8: + case 18: + case 19: + case 20: + feathersError = new errors.BadRequest(message) + break + case 2: + feathersError = new errors.Unavailable(message) + break + case 3: + case 23: + feathersError = new errors.Forbidden(message) + break + case 12: + feathersError = new errors.NotFound(message) + break + default: + feathersError = new errors.GeneralError(message) + break + } + } else if (typeof error.code === 'string' && error.severity && error.routine) { + // NOTE: Error codes taken from + // https://www.postgresql.org/docs/9.6/static/errcodes-appendix.html + // Omit query information + const messages = (error.message || '').split('-') + + error.message = messages[messages.length - 1] + + switch (error.code.slice(0, 2)) { + case '22': + feathersError = new errors.NotFound(message) + break + case '23': + feathersError = new errors.BadRequest(message) + break + case '28': + feathersError = new errors.Forbidden(message) + break + case '3D': + case '3F': + case '42': + feathersError = new errors.Unprocessable(message) + break + default: + feathersError = new errors.GeneralError(message) + break + } + } else if (!(error instanceof errors.FeathersError)) { + feathersError = new errors.GeneralError(message) + } + + feathersError[ERROR] = error + + throw feathersError +} diff --git a/packages/knex/src/index.ts b/packages/knex/src/index.ts new file mode 100644 index 0000000..8981cd7 --- /dev/null +++ b/packages/knex/src/index.ts @@ -0,0 +1,366 @@ +import { + AdapterInterface, + AdapterOptions, + AdapterParams, + AdapterQuery, + Id, + NullableId, + Paginated +} from '@wingshq/adapter-commons' +import { BadRequest, NotFound } from '@feathersjs/errors' +import { _ } from '@feathersjs/commons' +import { Knex } from 'knex' +import { errorHandler } from './error-handler' + +export * from './error-handler' + +export interface KnexOptions extends AdapterOptions { + Model: Knex + name: string + schema?: string +} + +export type KnexSettings = Omit<KnexOptions, 'id'> & { id?: string } + +export interface KnexParams<T> extends AdapterParams<T> { + Model?: Knex + name?: string + schema?: string + knex?: Knex.QueryBuilder + transaction?: Knex.Transaction +} + +const METHODS = { + $ne: 'whereNot', + $in: 'whereIn', + $nin: 'whereNotIn', + $or: 'orWhere', + $and: 'andWhere' +} + +const OPERATORS = { + $lt: '<', + $lte: '<=', + $gt: '>', + $gte: '>=', + $like: 'like', + $notlike: 'not like', + $ilike: 'ilike' +} + +const RETURNING_CLIENTS = ['postgresql', 'pg', 'oracledb', 'mssql'] + +export class KnexAdapter< + Result = unknown, + Data = Partial<Result>, + PatchData = Partial<Data>, + UpdateData = Data, + Params extends KnexParams<AdapterQuery<Result>> = KnexParams<AdapterQuery<Result>> +> implements AdapterInterface<Result, Data, PatchData, UpdateData, KnexOptions, Params> +{ + options: KnexOptions + + constructor(settings: KnexSettings) { + if (!settings || !settings.Model) { + throw new Error('You must provide a Model (the initialized Knex object)') + } + + if (typeof settings.name !== 'string') { + throw new Error('No table name specified.') + } + + this.options = { + id: 'id', + ...settings + } + } + + get id() { + return this.options.id + } + + get fullName() { + const { name, schema } = this.getOptions() + return schema ? `${schema}.${name}` : name + } + + getOptions(params?: Params): KnexOptions { + return { + ...this.options, + ...params + } + } + + getModel(params?: Params) { + const { Model } = this.getOptions(params) + return Model + } + + db(params?: Params) { + const { Model, name, schema } = this.getOptions(params) + + if (params?.transaction) { + const trx = params.transaction + // debug('ran %s with transaction %s', fullName, id) + return schema ? (trx.withSchema(schema).table(name) as Knex.QueryBuilder) : trx(name) + } + + return schema ? (Model.withSchema(schema).table(name) as Knex.QueryBuilder) : Model(name) + } + + knexify( + knexQuery: Knex.QueryBuilder, + query: { [key: string]: any } = {}, + parentKey?: string + ): Knex.QueryBuilder { + const knexify = this.knexify.bind(this) + + return Object.keys(query || {}).reduce((currentQuery, key) => { + const value = query[key] + + if (_.isObject(value)) { + return knexify(currentQuery, value, key) + } + + const column = parentKey || key + const method = METHODS[key as keyof typeof METHODS] + + if (method) { + if (key === '$or' || key === '$and') { + // This will create a nested query + currentQuery.where(function (this: any) { + for (const condition of value) { + this[method](function (this: Knex.QueryBuilder) { + knexify(this, condition) + }) + } + }) + + return currentQuery + } + + return (currentQuery as any)[method](column, value) + } + + const operator = OPERATORS[key as keyof typeof OPERATORS] || '=' + + return operator === '=' + ? currentQuery.where(column, value) + : currentQuery.where(column, operator, value) + }, knexQuery) + } + + createQuery(params?: Params) { + const { name, id } = this.getOptions(params) + const { filters, query } = this.filterQuery(params) + const builder = this.db(params) + + // $select uses a specific find syntax, so it has to come first. + if (filters.$select) { + const select = filters.$select.map((column) => + String(column).includes('.') ? column : `${name}.${String(column)}` + ) + // always select the id field, but make sure we only select it once + builder.select(...new Set([...select, `${name}.${id}`])) + } else { + builder.select(`${name}.*`) + } + + // build up the knex query out of the query params, include $and and $or filters + this.knexify(builder, { + ...query, + ..._.pick(filters, '$and', '$or') + }) + + // Handle $sort + if (filters.$sort) { + return Object.keys(filters.$sort).reduce( + (currentQuery, key) => currentQuery.orderBy(key, (filters.$sort as any)[key] === 1 ? 'asc' : 'desc'), + builder + ) + } + + return builder + } + + filterQuery(params?: Params) { + const { $select, $sort, $limit = null, $skip = 0, ...query } = params?.query || {} + + return { + filters: { $select, $sort, $limit, $skip }, + query + } + } + + async _findOrGet(id: NullableId, params?: Params) { + if (id !== null) { + const { name, id: idField } = this.getOptions(params) + const builder = params?.knex ? params.knex.clone() : this.createQuery(params) + const idQuery = builder.andWhere(`${name}.${idField}`, '=', id).catch(errorHandler) + + return idQuery as Promise<Result[]> + } + + return this.find({ + ...params, + paginate: false + }) + } + + async find(params: Params & { paginate: true }): Promise<Paginated<Result>> + async find(params?: Params & { paginate?: false }): Promise<Result[]> + async find(params?: Params & { paginate?: boolean }): Promise<Result[] | Paginated<Result>> { + const { filters } = this.filterQuery(params) + const { name, id } = this.getOptions(params) + const builder = params?.knex ? params.knex.clone() : this.createQuery(params) + const countBuilder = builder.clone().clearSelect().clearOrder().count(`${name}.${id} as total`) + + // Handle $limit + if (filters.$limit) { + builder.limit(filters.$limit) + } + + // Handle $skip + if (filters.$skip) { + builder.offset(filters.$skip) + } + + // provide default sorting if its not set + if (!filters.$sort && builder.client.driverName === 'mssql') { + builder.orderBy(`${name}.${id}`, 'asc') + } + + const data = filters.$limit === 0 ? [] : await builder.catch(errorHandler) + + if (params?.paginate === true) { + const total = await countBuilder.then((count) => parseInt(count[0] ? count[0].total : 0)) + + return { + total, + limit: filters.$limit, + skip: filters.$skip || 0, + data + } + } + + return data + } + + async get(id: Id, params?: Params): Promise<Result> { + const data = await this._findOrGet(id, params) + + if (data.length !== 1) { + throw new NotFound(`No record found for id '${id}'`) + } + + return data[0] + } + + async create(data: Data[], params?: Params): Promise<Result[]> + async create(data: Data, params?: Params): Promise<Result> + async create(_data: Data | Data[], params?: Params): Promise<Result[] | Result> { + const data = _data as any + + if (Array.isArray(data)) { + return Promise.all(data.map((current: Data) => this.create(current, params))) + } + + const { client } = this.db(params).client.config + const returning = RETURNING_CLIENTS.includes(client as string) ? [this.id] : [] + const rows: any = await this.db(params).insert(data, returning).catch(errorHandler) + const id = data[this.id] || rows[0][this.id] || rows[0] + + if (!id) { + return rows as Result[] + } + + return this.get(id, { + ...params, + query: _.pick(params?.query || {}, '$select') + }) + } + + async update(id: Id, _data: UpdateData, params?: Params): Promise<Result> { + if (id === null || Array.isArray(_data)) { + throw new BadRequest("You can not replace multiple instances. Did you mean 'patch'?") + } + + const data = _.omit(_data, this.id) + const oldData = await this.get(id, params) + const newObject = Object.keys(oldData).reduce((result: any, key) => { + if (key !== this.id) { + // We don't want the id field to be changed + result[key] = data[key] === undefined ? null : data[key] + } + + return result + }, {}) + + await this.db(params).update(newObject, '*').where(this.id, id) + + return this.get(id, params) + } + + async patch(id: Id, data: PatchData, params?: Params): Promise<Result> + async patch(id: null, data: PatchData, params?: Params): Promise<Result[]> + async patch(id: Id | null, raw: PatchData, params?: Params): Promise<Result[] | Result> { + const { name, id: idField } = this.getOptions(params) + const data = _.omit(raw, this.id) + const results = await this._findOrGet(id, { + ...params, + query: { + ...params?.query, + $select: [`${name}.${idField}`] + } + }) + const idList = results.map((current: any) => current[idField]) + const updateParams = { + ...params, + query: { + [`${name}.${idField}`]: { $in: idList }, + ...(params?.query?.$select ? { $select: params?.query?.$select } : {}) + } + } + const builder = this.createQuery(updateParams) + + await builder.update(data) + + const items = await this._findOrGet(null, updateParams) + + if (id !== null) { + if (items.length === 1) { + return items[0] + } else { + throw new NotFound(`No record found for id '${id}'`) + } + } + + return items + } + + async remove(id: Id, params?: Params): Promise<Result> + async remove(id: null, params?: Params): Promise<Result[]> + async remove(id: Id | null, params?: Params): Promise<Result[] | Result> { + const items = await this._findOrGet(id, params) + const { query } = this.filterQuery(params) + const q = this.db(params) + const idList = items.map((current: any) => current[this.id]) + + ;(query as any)[this.id] = { $in: idList } + + // build up the knex query out of the query params + this.knexify(q, query) + + await q.del().catch(errorHandler) + + if (id !== null) { + if (items.length === 1) { + return items[0] + } + + throw new NotFound(`No record found for id '${id}'`) + } + + return items + } +} diff --git a/packages/knex/test/connection.ts b/packages/knex/test/connection.ts new file mode 100644 index 0000000..b51a4f6 --- /dev/null +++ b/packages/knex/test/connection.ts @@ -0,0 +1,32 @@ +export const connection = (DB: string) => { + if (DB === 'mysql') { + return { + client: 'mysql', + connection: { + host: '127.0.0.1', + user: 'root', + password: '', + database: 'feathers_knex' + } + } + } + + if (DB === 'postgres') { + return { + client: 'postgresql', + connection: { + host: 'localhost', + database: 'feathers', + user: 'postgres', + password: 'postgres' + } + } + } + + return { + client: 'sqlite3', + connection: { + filename: './db.sqlite' + } + } +} diff --git a/packages/knex/test/error-handler.test.ts b/packages/knex/test/error-handler.test.ts new file mode 100644 index 0000000..c553def --- /dev/null +++ b/packages/knex/test/error-handler.test.ts @@ -0,0 +1,66 @@ +import { describe, it } from 'node:test' +import assert from 'assert' +import { errorHandler } from '../src' + +describe('Knex Error handler', () => { + it('sqlState', () => { + assert.throws( + () => + errorHandler({ + sqlState: '#23503' + }), + { + name: 'BadRequest' + } + ) + }) + + it('sqliteError', () => { + assert.throws( + () => + errorHandler({ + code: 'SQLITE_ERROR', + errno: 1 + }), + { + name: 'BadRequest' + } + ) + assert.throws(() => errorHandler({ code: 'SQLITE_ERROR', errno: 2 }), { name: 'Unavailable' }) + assert.throws(() => errorHandler({ code: 'SQLITE_ERROR', errno: 3 }), { name: 'Forbidden' }) + assert.throws(() => errorHandler({ code: 'SQLITE_ERROR', errno: 12 }), { name: 'NotFound' }) + assert.throws(() => errorHandler({ code: 'SQLITE_ERROR', errno: 13 }), { name: 'GeneralError' }) + }) + + it('postgresqlError', () => { + assert.throws( + () => + errorHandler({ + code: '22P02', + message: 'Key (id)=(1) is not present in table "users".', + severity: 'ERROR', + routine: 'ExecConstraints' + }), + { + name: 'NotFound' + } + ) + assert.throws( + () => + errorHandler({ code: '2874', message: 'Something', severity: 'ERROR', routine: 'ExecConstraints' }), + { + name: 'Forbidden' + } + ) + assert.throws( + () => + errorHandler({ code: '3D74', message: 'Something', severity: 'ERROR', routine: 'ExecConstraints' }), + { + name: 'Unprocessable' + } + ) + assert.throws(() => errorHandler({ code: 'XYZ', severity: 'ERROR', routine: 'ExecConstraints' }), { + name: 'GeneralError' + }) + }) +}) diff --git a/packages/knex/test/index.test.ts b/packages/knex/test/index.test.ts new file mode 100644 index 0000000..2deafc2 --- /dev/null +++ b/packages/knex/test/index.test.ts @@ -0,0 +1,396 @@ +import { describe, it, before, after, beforeEach, afterEach } from 'node:test' +import assert from 'assert' +import { adapterTests, Person } from '@wingshq/adapter-tests' +import knex from 'knex' +import { connection } from './connection' + +import { KnexAdapter } from '../src' +import { ERROR } from '../src/error-handler' + +const testSuite = adapterTests([ + '.id', + '.options', + '.get', + '.get + $select', + '.get + id + query', + '.get + NotFound', + '.get + id + query id', + '.find', + '.find + paginate + query', + '.find + $and', + '.find + $and + $or', + '.remove', + '.remove + $select', + '.remove + id + query', + '.remove + multi', + '.remove + multi no pagination', + '.remove + id + query id', + '.update', + '.update + $select', + '.update + id + query', + '.update + NotFound', + '.update + id + query id', + '.update + query + NotFound', + '.patch', + '.patch + $select', + '.patch + id + query', + '.patch multiple', + '.patch multiple no pagination', + '.patch multi query same', + '.patch multi query changed', + '.patch + query + NotFound', + '.patch + NotFound', + '.patch + id + query id', + '.create', + '.create ignores query', + '.create + $select', + '.create multi', + '.find + equal', + '.find + equal multiple', + '.find + $sort', + '.find + $limit', + '.find + $limit 0', + '.find + $skip', + '.find + $select', + '.find + $or', + '.find + $in', + '.find + $nin', + '.find + $lt', + '.find + $lte', + '.find + $gt', + '.find + $gte', + '.find + $ne', + '.find + $gt + $lt + $sort', + '.find + $or nested + $sort', + '.find + paginate', + '.find + paginate + $limit + $skip', + '.find + paginate + $limit 0', + '.find + paginate + params' +]) + +const TYPE = process.env.TEST_DB || 'sqlite' +const db = knex(connection(TYPE) as any) + +// Create a public database to mimic a "schema" +const schemaName = 'public' + +const people = new KnexAdapter<Person>({ + Model: db, + name: 'people' +}) + +const peopleId = new KnexAdapter<Person>({ + Model: db, + id: 'customid', + name: 'people-customid' +}) + +type Todo = { + id: number + text: string + personId: number + personName: string +} + +class TodoAdapter extends KnexAdapter<Todo> { + createQuery(params: any) { + const query = super.createQuery(params) + + query.join('people as person', 'todos.personId', 'person.id').select('person.name as personName') + + return query + } +} + +const todos = new TodoAdapter({ + Model: db, + name: 'todos' +}) + +const clean = async () => { + await db.schema.dropTableIfExists('todos') + await db.schema.dropTableIfExists(people.fullName) + await db.schema.createTable(people.fullName, (table) => { + table.increments('id') + table.string('name').notNullable() + table.integer('age') + table.integer('time') + table.boolean('created') + return table + }) + await db.schema.createTable('todos', (table) => { + table.increments('id') + table.string('text') + table.integer('personId') + return table + }) + await db.schema.dropTableIfExists(peopleId.fullName) + await db.schema.createTable(peopleId.fullName, (table) => { + table.increments('customid') + table.string('name') + table.integer('age') + table.integer('time') + table.boolean('created') + return table + }) +} + +describe('Wings knex Adapter', () => { + before(() => { + if (TYPE === 'sqlite') { + // Attach the public database to mimic a "schema" + db.schema.raw(`attach database '${schemaName}.sqlite' as ${schemaName}`) + } + }) + before(clean) + after(async () => { + await clean() + await db.destroy() + }) + + it('instantiated the adapter', () => { + assert.ok(people) + }) + + describe('$like method', () => { + let charlie: Person + + beforeEach(async () => { + charlie = await people.create({ + name: 'Charlie Brown', + age: 10 + }) + }) + + afterEach(() => people.remove(charlie.id)) + + it('$like in query', async () => { + const data = await people.find({ + paginate: false, + query: { name: { $like: '%lie%' } } as any + }) + + assert.strictEqual(data[0].name, 'Charlie Brown') + }) + }) + + describe('$notlike method', () => { + let hasMatch: Person + let hasNoMatch: Person + + beforeEach(async () => { + hasMatch = await people.create({ + name: 'XYZabcZYX' + }) + hasNoMatch = await people.create({ + name: 'XYZZYX' + }) + }) + + afterEach(() => { + people.remove(hasMatch.id) + people.remove(hasNoMatch.id) + }) + + it('$notlike in query', async () => { + const data = await people.find({ + paginate: false, + query: { name: { $notlike: '%abc%' } } as any + }) + + assert.strictEqual(data.length, 1) + assert.strictEqual(data[0].name, 'XYZZYX') + }) + }) + + describe('adapter specifics', () => { + let daves: Person[] + + beforeEach(async () => { + daves = await Promise.all([ + people.create({ + name: 'Ageless', + age: null + }), + people.create({ + name: 'Dave', + age: 32 + }), + people.create({ + name: 'Dada', + age: 1 + }) + ]) + }) + + afterEach(async () => { + try { + await people.remove(daves[0].id) + await people.remove(daves[1].id) + await people.remove(daves[2].id) + } catch (error: unknown) {} + }) + + it('$or works properly (#120)', async () => { + const data = await people.find({ + paginate: false, + query: { + name: 'Dave', + $or: [ + { + age: 1 + }, + { + age: 32 + } + ] + } + }) + + assert.strictEqual(data.length, 1) + assert.strictEqual(data[0].name, 'Dave') + assert.strictEqual(data[0].age, 32) + }) + + it('$and works properly', async () => { + const data = await people.find({ + paginate: false, + query: { + $and: [ + { + $or: [{ name: 'Dave' }, { name: 'Dada' }] + }, + { + age: { $lt: 23 } + } + ] + } + }) + + assert.strictEqual(data.length, 1) + assert.strictEqual(data[0].name, 'Dada') + assert.strictEqual(data[0].age, 1) + }) + + it('where conditions support NULL values properly', async () => { + const data = await people.find({ + query: { + age: null + } + }) + + assert.strictEqual(data.length, 1) + assert.strictEqual(data[0].name, 'Ageless') + assert.strictEqual(data[0].age, null) + }) + + it('where conditions support NOT NULL case properly', async () => { + const data = await people.find({ + paginate: false, + query: { + age: { $ne: null } + } + }) + + assert.strictEqual(data.length, 2) + assert.notStrictEqual(data[0].name, 'Ageless') + assert.notStrictEqual(data[0].age, null) + assert.notStrictEqual(data[1].name, 'Ageless') + assert.notStrictEqual(data[1].age, null) + }) + + it('where conditions support NULL values within AND conditions', async () => { + const data = await people.find({ + paginate: false, + query: { + age: null, + name: 'Ageless' + } + }) + + assert.strictEqual(data.length, 1) + assert.strictEqual(data[0].name, 'Ageless') + assert.strictEqual(data[0].age, null) + }) + + it('where conditions support NULL values within OR conditions', async () => { + const data = await people.find({ + paginate: false, + query: { + $or: [ + { + age: null + }, + { + name: 'Dada' + } + ] + } + }) + + assert.strictEqual(data.length, 2) + assert.notStrictEqual(data[0].name, 'Dave') + assert.notStrictEqual(data[0].age, 32) + assert.notStrictEqual(data[1].name, 'Dave') + assert.notStrictEqual(data[1].age, 32) + }) + + it('attaches the SQL error', async () => { + await assert.rejects( + () => people.create({}), + (error: any) => { + assert.ok(error[ERROR]) + return true + } + ) + }) + + it('get by id works with `createQuery` as params.knex', async () => { + const knex = people.createQuery() + const dave = await people.get(daves[0].id, { knex }) + + assert.deepStrictEqual(dave, daves[0]) + }) + }) + + describe('associations', () => { + it('create, query and get with associations, can unambigiously $select', async () => { + const dave = await people.create({ + name: 'Dave', + age: 133 + }) + const todo = await todos.create({ + text: 'Do dishes', + personId: dave.id + }) + + const [found] = await todos.find({ + paginate: false, + query: { + 'person.age': { $gt: 100 } + } as any + }) + const got = await todos.get(todo.id) + + assert.deepStrictEqual( + await todos.get(todo.id, { + query: { $select: ['id', 'text'] } + }), + { + id: todo.id, + text: todo.text, + personName: 'Dave' + } + ) + assert.strictEqual(got.personName, dave.name) + assert.deepStrictEqual(got, todo) + assert.deepStrictEqual(found, todo) + + await people.remove(null) + await todos.remove(null) + }) + }) + + testSuite(people, 'id') + testSuite(peopleId, 'customid') +}) diff --git a/packages/knex/tsconfig.json b/packages/knex/tsconfig.json new file mode 100644 index 0000000..f8a7bc1 --- /dev/null +++ b/packages/knex/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig", + "include": [ + "src/**/*.ts" + ], + "compilerOptions": { + "outDir": "lib" + } +} \ No newline at end of file