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