├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .mocharc.json ├── .prettierignore ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── compose.yml ├── package.json ├── pnpm-lock.yaml ├── scripts └── dist-fix.js ├── src ├── dialect │ ├── adapter.ts │ ├── errors.ts │ ├── http │ │ ├── http-connection.ts │ │ ├── http-dialect.ts │ │ ├── http-driver.ts │ │ ├── http-errors.ts │ │ ├── http-introspector.ts │ │ └── http-types.ts │ ├── shared.ts │ └── websockets │ │ ├── websockets-connection.ts │ │ ├── websockets-dialect.ts │ │ ├── websockets-driver.ts │ │ ├── websockets-introspector.ts │ │ └── websockets-types.ts ├── helpers │ └── index.ts ├── index.ts ├── operation-node │ ├── create-query-node.ts │ ├── else-if-node.ts │ ├── if-else-query-node.ts │ ├── operation-node.ts │ ├── relate-query-node.ts │ ├── return-node.ts │ └── vertex-node.ts ├── parser │ ├── create-object-parser.ts │ ├── return-parser.ts │ └── vertex-expression-parser.ts ├── query-builder │ ├── create-query-builder.ts │ ├── if-else-query-builder.ts │ ├── relate-query-builder.ts │ ├── return-interface.ts │ └── set-content-interface.ts ├── query-compiler │ └── query-compiler.ts ├── surreal-kysely.ts └── util │ ├── encode-to-base64.ts │ ├── object-utils.ts │ ├── prevent-await.ts │ ├── query-id.ts │ ├── random-string.ts │ ├── surreal-types.ts │ └── type-utils.ts ├── tests └── nodejs │ ├── http-dialect.test.ts │ └── surreal-kysely │ ├── basic-types.test.ts │ ├── create.test.ts │ ├── if-else.test.ts │ ├── record-ids.test.ts │ ├── relate.test.ts │ ├── shared.ts │ └── transaction.test.ts ├── tsconfig.json └── tsup.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | assets 4 | .prettierrc.js 5 | jest.config.js 6 | package.json 7 | pnpm-lock.yaml 8 | README.md 9 | tsconfig.json 10 | tsup.config.ts 11 | scripts 12 | examples 13 | tests -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["prettier"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": {"project": ["./tsconfig.json"]}, 6 | "plugins": ["prettier", "import", "@typescript-eslint"], 7 | "rules": { 8 | "prettier/prettier": ["error"], 9 | "import/extensions": [2, "ignorePackages"] 10 | }, 11 | "settings": { 12 | "import/extensions": [".ts"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | reviewers: 13 | - "igalklebanov" 14 | versioning-strategy: increase 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | branches: [main] 5 | push: 6 | branches: [main] 7 | jobs: 8 | run-node-tests: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [14.x, 16.x, 18.x] 14 | fail-fast: false 15 | 16 | steps: 17 | - name: Spin up SurrealDB image 18 | run: | 19 | docker run -d -p 8000:8000 surrealdb/surrealdb:latest start --user root --pass root memory 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Install Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install pnpm 30 | uses: pnpm/action-setup@v2.2.2 31 | with: 32 | version: 7 33 | run_install: false 34 | 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | run: | 38 | echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" 39 | 40 | - name: Setup pnpm cache 41 | uses: actions/cache@v3 42 | with: 43 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 44 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 45 | restore-keys: | 46 | ${{ runner.os }}-pnpm-store- 47 | 48 | - name: Install dependencies 49 | run: pnpm i 50 | 51 | - name: Build 52 | run: pnpm run build 53 | 54 | - name: Test 55 | run: pnpm run test:nodejs 56 | 57 | run-misc-checks: 58 | runs-on: ubuntu-latest 59 | 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v3 63 | 64 | - name: Install Node.js 65 | uses: actions/setup-node@v3 66 | with: 67 | node-version: 16.x 68 | 69 | - name: Install pnpm 70 | uses: pnpm/action-setup@v2.2.2 71 | with: 72 | version: 7 73 | run_install: false 74 | 75 | - name: Get pnpm store directory 76 | id: pnpm-cache 77 | run: | 78 | echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" 79 | 80 | - name: Setup pnpm cache 81 | uses: actions/cache@v3 82 | with: 83 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 84 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 85 | restore-keys: | 86 | ${{ runner.os }}-pnpm-store- 87 | 88 | - name: Install dependencies 89 | run: pnpm i 90 | 91 | - name: Build 92 | run: pnpm run build 93 | 94 | - name: Type Check 95 | run: pnpm run type-check 96 | 97 | - name: Lint 98 | run: pnpm run lint 99 | 100 | # - name: Test ESM 101 | # run: cd examples/esm && npm i && npm start 102 | 103 | # - name: Test CJS 104 | # run: cd examples/cjs && npm i && npm start 105 | 106 | # run-browser-tests: 107 | # runs-on: ubuntu-latest 108 | 109 | # services: 110 | # surrealdb: 111 | # image: surrealdb/surrealdb:latest 112 | # ports: 113 | # - 8000:8000 114 | # options: >- 115 | # --health-cmd "start --log trace --user root --pass root memory" 116 | # --health-interval 10s 117 | # --health-timeout 5s 118 | # --health-retries 5 119 | 120 | # steps: 121 | # - name: Checkout 122 | # uses: actions/checkout@v3 123 | 124 | # - name: Install Node.js 125 | # uses: actions/setup-node@v3 126 | # with: 127 | # node-version: 16.x 128 | 129 | # - name: Install pnpm 130 | # uses: pnpm/action-setup@v2.2.2 131 | # with: 132 | # version: 7 133 | # run_install: false 134 | 135 | # - name: Get pnpm store directory 136 | # id: pnpm-cache 137 | # run: | 138 | # echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" 139 | 140 | # - name: Setup pnpm cache 141 | # uses: actions/cache@v3 142 | # with: 143 | # path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 144 | # key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 145 | # restore-keys: | 146 | # ${{ runner.os }}-pnpm-store- 147 | 148 | # - name: Install dependencies 149 | # run: pnpm i && pnpx playwright install 150 | 151 | # - name: Build 152 | # run: pnpm run build 153 | 154 | # - name: Run browser tests 155 | # run: pnpm run test:browser 156 | 157 | # run-deno-tests: 158 | # runs-on: ubuntu-latest 159 | 160 | # services: 161 | # surrealdb: 162 | # image: surrealdb/surrealdb:latest 163 | # ports: 164 | # - 8000:8000 165 | # options: >- 166 | # --health-cmd "start --log trace --user root --pass root memory" 167 | # --health-interval 10s 168 | # --health-timeout 5s 169 | # --health-retries 5 170 | 171 | # steps: 172 | # - name: Checkout 173 | # uses: actions/checkout@v3 174 | 175 | # - name: Install Node.js 176 | # uses: actions/setup-node@v3 177 | # with: 178 | # node-version: 16.x 179 | 180 | # - name: Install Deno 181 | # uses: denolib/setup-deno@v2 182 | # with: 183 | # deno-version: 1.26.x 184 | 185 | # - name: Install pnpm 186 | # uses: pnpm/action-setup@v2.2.2 187 | # with: 188 | # version: 7 189 | # run_install: false 190 | 191 | # - name: Get pnpm store directory 192 | # id: pnpm-cache 193 | # run: | 194 | # echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" 195 | 196 | # - name: Setup pnpm cache 197 | # uses: actions/cache@v3 198 | # with: 199 | # path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 200 | # key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 201 | # restore-keys: | 202 | # ${{ runner.os }}-pnpm-store- 203 | 204 | # - name: Install dependencies 205 | # run: pnpm i 206 | 207 | # - name: Build 208 | # run: pnpm run build 209 | 210 | # - name: Test 211 | # run: pnpm run test:deno 212 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | *.log 5 | .env* 6 | helpers/ -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "recursive": true, 4 | "require": ["esbuild-runner/register"], 5 | "timeout": 30000 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | semi: false, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | bracketSpacing: false, 7 | plugins: [require('prettier-plugin-organize-imports'), require('prettier-plugin-pkg')], 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enablePaths": ["tests/deno"], 3 | "files.eol": "\n", 4 | "prettier.prettierPath": "node_modules/prettier/index.js", 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "typescript.enablePromptUseWorkspaceTsdk": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Igal Klebanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kysely-surrealdb 2 | 3 | ![Powered by TypeScript](https://img.shields.io/badge/powered%20by-typescript-blue.svg) 4 | 5 | [Kysely](https://github.com/koskimas/kysely) dialects, plugins and other goodies for [SurrealDB](https://www.surrealdb.com/). 6 | 7 | [SurrealQL](https://surrealdb.com/docs/surrealql) is based on SQL, so why not? :trollface: 8 | 9 | ## Installation 10 | 11 | #### NPM 7+ 12 | 13 | ```bash 14 | npm i kysely-surrealdb 15 | ``` 16 | 17 | #### NPM <7 18 | 19 | ```bash 20 | npm i kysely-surrealdb kysely surrealdb.js 21 | ``` 22 | 23 | #### Yarn 24 | 25 | ```bash 26 | yarn add kysely-surrealdb kysely surrealdb.js 27 | ``` 28 | 29 | #### PNPM 30 | 31 | ```bash 32 | pnpm add kysely-surrealdb kysely surrealdb.js 33 | ``` 34 | 35 | > `surrealdb.js` is an optional peer dependency. It's only needed if you want to use `SurrealDbWebSocketsDialect`. If you don't need it, you can remove `surreal.js` from the install commands above. 36 | 37 | ### Deno 38 | 39 | This package uses/extends some [Kysely](https://github.com/koskimas/kysely) types and classes, which are imported using its NPM package name -- not a relative file path or CDN url. 40 | 41 | `SurrealDbWebSocketsDialect` uses `surrealdb.js` which is imported using its NPM package name -- not a relative file path or CDN url. 42 | 43 | To fix that, add an [`import_map.json`](https://deno.land/manual@v1.26.1/linking_to_external_code/import_maps) file. 44 | 45 | ```json 46 | { 47 | "imports": { 48 | "kysely": "npm:kysely@^0.25.0", 49 | "surrealdb.js": "https://deno.land/x/surrealdb@v0.5.0" // optional - only if you're using `SurrealDbWebSocketsDialect` 50 | } 51 | } 52 | ``` 53 | 54 | ## Usage 55 | 56 | ### HTTP Dialect 57 | 58 | [SurrealDB](https://www.surrealdb.com/)'s [HTTP endpoints](https://surrealdb.com/docs/integration/http) allow executing [SurrealQL](https://surrealdb.com/docs/surrealql) queries in the browser and are a great fit for serverless functions and other auto-scaling compute services. 59 | 60 | #### Node.js 16.8+ 61 | 62 | Older node versions are supported as well, just swap [`undici`](https://github.com/nodejs/undici) with [`node-fetch`](https://github.com/node-fetch/node-fetch). 63 | 64 | ```ts 65 | import {Kysely} from 'kysely' 66 | import {SurrealDatabase, SurrealDbHttpDialect, type SurrealEdge} from 'kysely-surrealdb' 67 | import {fetch} from 'undici' 68 | 69 | interface Database { 70 | person: { 71 | first_name: string | null 72 | last_name: string | null 73 | age: number 74 | } 75 | own: SurrealEdge<{ 76 | time: { 77 | adopted: string 78 | } | null 79 | }> 80 | pet: { 81 | name: string 82 | owner_id: string | null 83 | } 84 | } 85 | 86 | const db = new Kysely>({ 87 | dialect: new SurrealDbHttpDialect({ 88 | database: '', 89 | fetch, 90 | hostname: '', // e.g. 'localhost:8000' 91 | namespace: '', 92 | password: '', 93 | username: '', 94 | }), 95 | }) 96 | ``` 97 | 98 | ### WebSockets Dialect 99 | 100 | ```ts 101 | import {Kysely} from 'kysely' 102 | import {SurrealDatabase, SurrealDbWebSocketsDialect, type SurrealEdge} from 'kysely-surrealdb' 103 | import Surreal from 'surrealdb.js' 104 | 105 | interface Database { 106 | person: { 107 | first_name: string | null 108 | last_name: string | null 109 | age: number 110 | } 111 | own: SurrealEdge<{ 112 | time: { 113 | adopted: string 114 | } | null 115 | }> 116 | pet: { 117 | name: string 118 | owner_id: string | null 119 | } 120 | } 121 | 122 | // with username and password 123 | const db = new Kysely>({ 124 | dialect: new SurrealDbWebSocketsDialect({ 125 | database: '', 126 | Driver: Surreal, 127 | hostname: '', // e.g. 'localhost:8000' 128 | namespace: '', 129 | password: '', 130 | // scope: '', // optional 131 | username: '', 132 | }), 133 | }) 134 | 135 | // alternatively, with a token 136 | const dbWithToken = new Kysely>({ 137 | dialect: new SurrealDbWebSocketsDialect({ 138 | database: '', 139 | Driver: Surreal, 140 | hostname: '', // e.g. 'localhost:8000' 141 | namespace: '', 142 | token: '', 143 | }), 144 | }) 145 | ``` 146 | 147 | ### SurrealKysely Query Builder 148 | 149 | The awesomeness of Kysely, with some SurrealQL query builders patched in. 150 | 151 | > This example uses `SurrealDbHttpDialect` but `SurrealDbWebSocketsDialect` works just as well. 152 | 153 | ```ts 154 | import {SurrealDbHttpDialect, SurrealKysely, type SurrealEdge} from 'kysely-surrealdb' 155 | import {fetch} from 'undici' 156 | 157 | interface Database { 158 | person: { 159 | first_name: string | null 160 | last_name: string | null 161 | age: number 162 | } 163 | own: SurrealEdge<{ 164 | time: { 165 | adopted: string 166 | } | null 167 | }> 168 | pet: { 169 | name: string 170 | owner_id: string | null 171 | } 172 | } 173 | 174 | const db = new SurrealKysely({ 175 | dialect: new SurrealDbHttpDialect({ 176 | database: '', 177 | fetch, 178 | hostname: '', 179 | namespace: '', 180 | password: '', 181 | username: '', 182 | }), 183 | }) 184 | 185 | await db 186 | .create('person:100') 187 | .set({ 188 | first_name: 'Jennifer', 189 | age: 15, 190 | }) 191 | .return('none') 192 | .execute() 193 | ``` 194 | 195 | #### Supported SurrealQL specific statements: 196 | 197 | [create](https://surrealdb.com/docs/surrealql/statements/create), 198 | [if else](https://surrealdb.com/docs/surrealql/statements/ifelse), 199 | [relate](https://surrealdb.com/docs/surrealql/statements/relate). 200 | 201 | #### Why not write a query builder from scratch 202 | 203 | Kysely is growing to be THE sql query builder solution in the typescript ecosystem. 204 | Koskimas' dedication, attention to detail, experience from creating objection.js, project structure, simplicity, design patterns and philosophy, 205 | made adding code to that project a really good experience as a contributor. Taking 206 | what's great about that codebase, and patching in SurrealQL stuff seems like an easy 207 | win in the short-medium term. 208 | 209 | ## License 210 | 211 | MIT License, see `LICENSE` 212 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | surrealdb: 3 | image: surrealdb/surrealdb:latest 4 | ports: 5 | - '8000:8000' 6 | volumes: 7 | - ./data:/data 8 | command: start --log trace --user root --pass root memory 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kysely-surrealdb", 3 | "version": "0.7.4", 4 | "description": "Kysely dialects, plugins and other goodies for SurrealDB", 5 | "repository": "https://github.com/igalklebanov/kysely-surrealdb.git", 6 | "homepage": "https://github.com/igalklebanov/kysely-surrealdb", 7 | "author": "Igal Klebanov ", 8 | "license": "MIT", 9 | "main": "./dist/cjs/index.js", 10 | "module": "./dist/esm/index.js", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/esm/index.js", 14 | "require": "./dist/cjs/index.js", 15 | "default": "./dist/cjs/index.js" 16 | }, 17 | "./helpers": { 18 | "import": "./dist/esm/helpers/index.js", 19 | "require": "./dist/cjs/helpers/index.js", 20 | "default": "./dist/cjs/helpers/index.js" 21 | } 22 | }, 23 | "files": [ 24 | "dist", 25 | "helpers" 26 | ], 27 | "keywords": [ 28 | "kysely", 29 | "surrealdb", 30 | "driver", 31 | "dialect", 32 | "plugin" 33 | ], 34 | "scripts": { 35 | "build": "tsup && node ./scripts/dist-fix.js", 36 | "clean": "rm -rf dist", 37 | "fmt": "prettier --write .", 38 | "fmt:check": "prettier --check .", 39 | "lint": "eslint src --ext .ts", 40 | "prepack": "pnpm run lint && pnpm run build", 41 | "test:nodejs": "mocha ./tests/nodejs", 42 | "type-check": "tsc --noEmit" 43 | }, 44 | "peerDependencies": { 45 | "kysely": ">= 0.24.2 < 1", 46 | "surrealdb.js": "^0.7.3" 47 | }, 48 | "peerDependenciesMeta": { 49 | "surrealdb.js": { 50 | "optional": true 51 | } 52 | }, 53 | "devDependencies": { 54 | "@tsconfig/node16": "^1.0.3", 55 | "@types/chai": "^4.3.3", 56 | "@types/chai-as-promised": "^7.1.5", 57 | "@types/fs-extra": "^11.0.1", 58 | "@types/mocha": "^10.0.0", 59 | "@types/node": "^16", 60 | "@typescript-eslint/eslint-plugin": "^5.38.0", 61 | "@typescript-eslint/parser": "^5.38.0", 62 | "chai": "^4.3.6", 63 | "chai-as-promised": "^7.1.1", 64 | "esbuild": "^0.15.11", 65 | "esbuild-runner": "^2.2.2", 66 | "eslint": "^8.24.0", 67 | "eslint-config-prettier": "^8.5.0", 68 | "eslint-plugin-import": "^2.26.0", 69 | "eslint-plugin-prettier": "^4.2.1", 70 | "fs-extra": "^11.1.1", 71 | "kysely": "^0.24.2", 72 | "mocha": "^10.0.0", 73 | "node-fetch": "^2", 74 | "prettier": "^2.7.1", 75 | "prettier-plugin-organize-imports": "^3.1.1", 76 | "prettier-plugin-pkg": "^0.17.1", 77 | "surrealdb.js": "^0.7.3", 78 | "tsup": "^6.7.0", 79 | "typescript": "^4.8.4", 80 | "undici": "^5.11.0" 81 | }, 82 | "sideEffects": false 83 | } 84 | -------------------------------------------------------------------------------- /scripts/dist-fix.js: -------------------------------------------------------------------------------- 1 | const {mkdir, readdir, rename, rm, writeFile, copyFile, readFile, unlink, move} = require('fs-extra') 2 | const path = require('node:path') 3 | const packageJson = require('../package.json') 4 | 5 | ;(async () => { 6 | const distPath = path.join(__dirname, '../dist') 7 | const distCjsPath = path.join(distPath, 'cjs') 8 | const distEsmPath = path.join(distPath, 'esm') 9 | const distEsmHelpersPath = path.join(distEsmPath, 'helpers') 10 | const distHelpersPath = path.join(distPath, 'helpers') 11 | 12 | const [distSubpaths, distEsmSubpaths, distEsmHelpersSubpaths, distHelpersSubpaths] = await Promise.all([ 13 | readdir(distPath), 14 | readdir(distEsmPath), 15 | readdir(distEsmHelpersPath), 16 | readdir(distHelpersPath), 17 | rm(distCjsPath, {force: true, recursive: true}), 18 | writeDummyExportsFiles(), 19 | ]) 20 | 21 | await Promise.all([ 22 | mkdir(distCjsPath), 23 | writePackageJsonFile(distEsmPath, 'module'), 24 | ...copyDtsFiles(distPath, distSubpaths, distEsmPath), 25 | ...copyDtsFiles(distHelpersPath, distHelpersSubpaths, distEsmHelpersPath), 26 | ...addReferenceTypesTripleDash(distEsmPath, distEsmSubpaths), 27 | ...addReferenceTypesTripleDash(distEsmHelpersPath, distEsmHelpersSubpaths), 28 | ]) 29 | 30 | await Promise.all([ 31 | writePackageJsonFile(distCjsPath, 'commonjs'), 32 | ...distSubpaths 33 | .filter((filePath) => filePath.match(/\.[t|j]s(\.map)?$/)) 34 | .map((filePath) => rename(path.join(distPath, filePath), path.join(distCjsPath, filePath))), 35 | move(distHelpersPath, path.join(distCjsPath, 'helpers')), 36 | ]) 37 | })() 38 | 39 | function writePackageJsonFile(destinationPath, type) { 40 | return writeFile(path.join(destinationPath, 'package.json'), JSON.stringify({type, sideEffects: false})) 41 | } 42 | 43 | function copyDtsFiles(sourcePath, sourceSubpaths, destinationPath) { 44 | return sourceSubpaths 45 | .filter((sourceSubpath) => sourceSubpath.match(/\.d\.ts$/)) 46 | .map((dtsFilename) => copyFile(path.join(sourcePath, dtsFilename), path.join(destinationPath, dtsFilename))) 47 | } 48 | 49 | function addReferenceTypesTripleDash(folderPath, folderContentPaths) { 50 | return folderContentPaths 51 | .filter((contentPath) => contentPath.match(/\.js$/)) 52 | .map(async (filename) => { 53 | const filePath = path.join(folderPath, filename) 54 | 55 | const file = await readFile(filePath) 56 | const fileContents = file.toString() 57 | 58 | const dtsFilePath = `./${filename.replace('.js', '.d.ts')}` 59 | 60 | const denoFriendlyFileContents = [`/// `, fileContents].join('\n') 61 | 62 | await writeFile(filePath, denoFriendlyFileContents) 63 | }) 64 | } 65 | 66 | async function writeDummyExportsFiles() { 67 | const rootPath = path.join(__dirname, '..') 68 | 69 | await Promise.all( 70 | Object.entries(packageJson.exports) 71 | .filter(([exportPath]) => exportPath !== '.') 72 | .flatMap(async ([exportPath, exportConfig]) => { 73 | const [, ...dummyPathParts] = exportPath.split('/') 74 | const dummyFilename = dummyPathParts.length > 1 ? dummyPathParts.pop() : 'index' 75 | 76 | const [, ...destinationFolders] = exportConfig.require.split('/').slice(0, -1) 77 | 78 | const dummyFolderPathFromRoot = path.join(rootPath, ...dummyPathParts) 79 | 80 | await mkdir(dummyFolderPathFromRoot, {recursive: true}) 81 | 82 | const dummyFilePathFromRoot = path.join(dummyFolderPathFromRoot, dummyFilename) 83 | const actualPath = path.relative(dummyFolderPathFromRoot, path.join(rootPath, ...destinationFolders)) 84 | 85 | return [ 86 | writeFile(dummyFilePathFromRoot + '.js', `module.exports = require('${actualPath}')`), 87 | writeFile(dummyFilePathFromRoot + '.d.ts', `export * from '${actualPath}'`), 88 | ] 89 | }), 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /src/dialect/adapter.ts: -------------------------------------------------------------------------------- 1 | import {DialectAdapterBase, type Kysely} from 'kysely' 2 | 3 | import {SurrealDbLocksUnsupportedError} from './errors.js' 4 | 5 | export class SurrealDbAdapter extends DialectAdapterBase { 6 | get supportsReturning(): boolean { 7 | return false 8 | } 9 | 10 | get supportsTransactionalDdl(): boolean { 11 | return false 12 | } 13 | 14 | async acquireMigrationLock(_: Kysely): Promise { 15 | this.#throwLocksError() 16 | } 17 | 18 | async releaseMigrationLock(_: Kysely): Promise { 19 | this.#throwLocksError() 20 | } 21 | 22 | #throwLocksError(): never { 23 | throw new SurrealDbLocksUnsupportedError() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/dialect/errors.ts: -------------------------------------------------------------------------------- 1 | import type {CompiledQuery} from 'kysely' 2 | 3 | export class SurrealDbLocksUnsupportedError extends Error { 4 | constructor() { 5 | super('Locks are not supported!') 6 | this.name = 'SurrealDbLocksUnsupportedError' 7 | } 8 | } 9 | 10 | export class SurrealDbMultipleStatementQueriesUnsupportedError extends Error { 11 | constructor() { 12 | super('Multiple statement queries are not supported!') 13 | this.name = 'SurrealDbMultipleStatementsUnsupportedError' 14 | } 15 | } 16 | 17 | export class SurrealDbSchemasUnsupportedError extends Error { 18 | constructor() { 19 | super('Schemas are not supported!') 20 | this.name = 'SurrealDbSchemasUnsupportedError' 21 | } 22 | } 23 | 24 | export class SurrealDbStreamingUnsupportedError extends Error { 25 | constructor() { 26 | super('SurrealDB does not support streaming!') 27 | this.name = 'SurrealDbStreamingUnsupportedError' 28 | } 29 | } 30 | 31 | export class SurrealDbDatabaseError extends Error { 32 | constructor(message: string = 'Something went wrong!') { 33 | super(message) 34 | this.name = 'SurrealDbDatabaseError' 35 | } 36 | } 37 | 38 | export function assertSingleStatementQuery(compiledQuery: CompiledQuery): void { 39 | if (compiledQuery.sql.match(/.*;.+/i)) { 40 | throw new SurrealDbMultipleStatementQueriesUnsupportedError() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/dialect/http/http-connection.ts: -------------------------------------------------------------------------------- 1 | import type {CompiledQuery, DatabaseConnection, QueryResult} from 'kysely' 2 | 3 | import {assertSingleStatementQuery, SurrealDbDatabaseError, SurrealDbStreamingUnsupportedError} from '../errors.js' 4 | import {serializeQuery} from '../shared.js' 5 | import type {SurrealDbHttpDialectConfig, SurrealDbHttpRequestHeaders, SurrealDbHttpResponseBody} from './http-types.js' 6 | 7 | export class SurrealDbHttpConnection implements DatabaseConnection { 8 | readonly #basePath: string 9 | readonly #config: SurrealDbHttpDialectConfig 10 | readonly #requestHeaders: SurrealDbHttpRequestHeaders 11 | 12 | constructor(config: SurrealDbHttpDialectConfig, basePath: string, requestHeaders: SurrealDbHttpRequestHeaders) { 13 | this.#basePath = basePath 14 | this.#config = config 15 | this.#requestHeaders = requestHeaders 16 | } 17 | 18 | async executeQuery(compiledQuery: CompiledQuery): Promise> { 19 | assertSingleStatementQuery(compiledQuery) 20 | 21 | const body = serializeQuery(compiledQuery) 22 | 23 | const response = await this.#config.fetch(`${this.#basePath}/sql`, { 24 | body, 25 | headers: this.#requestHeaders, 26 | method: 'POST', 27 | }) 28 | 29 | if (!response.ok) { 30 | throw new SurrealDbDatabaseError(await response.text()) 31 | } 32 | 33 | const responseBody = await response.json() 34 | 35 | const queryResult = (responseBody as SurrealDbHttpResponseBody).pop() 36 | 37 | if (queryResult?.status === 'ERR') { 38 | throw new SurrealDbDatabaseError(queryResult.detail) 39 | } 40 | 41 | const rows = queryResult?.result || [] 42 | 43 | return { 44 | numAffectedRows: BigInt(rows.length), 45 | rows, 46 | } 47 | } 48 | 49 | async *streamQuery(_: CompiledQuery): AsyncIterableIterator> { 50 | throw new SurrealDbStreamingUnsupportedError() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/dialect/http/http-dialect.ts: -------------------------------------------------------------------------------- 1 | import type {DatabaseIntrospector, Dialect, DialectAdapter, Driver, Kysely, QueryCompiler} from 'kysely' 2 | 3 | import {SurrealDbQueryCompiler} from '../../query-compiler/query-compiler.js' 4 | import {SurrealDbAdapter} from '../adapter.js' 5 | import {SurrealDbHttpDriver} from './http-driver.js' 6 | import {SurrealDbHttpIntrospector} from './http-introspector.js' 7 | import type {SurrealDbHttpDialectConfig} from './http-types.js' 8 | 9 | export class SurrealDbHttpDialect implements Dialect { 10 | readonly #config: SurrealDbHttpDialectConfig 11 | 12 | constructor(config: SurrealDbHttpDialectConfig) { 13 | this.#config = config 14 | } 15 | 16 | createAdapter(): DialectAdapter { 17 | return new SurrealDbAdapter() 18 | } 19 | 20 | createDriver(): Driver { 21 | return new SurrealDbHttpDriver(this.#config) 22 | } 23 | 24 | createIntrospector(db: Kysely): DatabaseIntrospector { 25 | return new SurrealDbHttpIntrospector(db) 26 | } 27 | 28 | createQueryCompiler(): QueryCompiler { 29 | return new SurrealDbQueryCompiler() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/dialect/http/http-driver.ts: -------------------------------------------------------------------------------- 1 | import type {DatabaseConnection, Driver} from 'kysely' 2 | 3 | import {encodeToBase64} from '../../util/encode-to-base64.js' 4 | import {resolveBasePath} from '../shared.js' 5 | import {SurrealDbHttpConnection} from './http-connection.js' 6 | import {SurrealDbHttpTransactionsUnsupportedError} from './http-errors.js' 7 | import type {SurrealDbHttpDialectConfig, SurrealDbHttpRequestHeaders} from './http-types.js' 8 | 9 | export class SurrealDbHttpDriver implements Driver { 10 | readonly #basePath: string 11 | readonly #config: SurrealDbHttpDialectConfig 12 | readonly #requestHeaders: SurrealDbHttpRequestHeaders 13 | 14 | constructor(config: SurrealDbHttpDialectConfig) { 15 | this.#config = config 16 | this.#basePath = resolveBasePath(config.hostname) 17 | this.#requestHeaders = this.#createRequestHeaders() 18 | } 19 | 20 | async init(): Promise { 21 | // noop 22 | } 23 | 24 | async acquireConnection(): Promise { 25 | return new SurrealDbHttpConnection(this.#config, this.#basePath, this.#requestHeaders) 26 | } 27 | 28 | async beginTransaction(): Promise { 29 | throw this.#throwTransactionsError() 30 | } 31 | 32 | async commitTransaction(): Promise { 33 | throw this.#throwTransactionsError() 34 | } 35 | 36 | async rollbackTransaction(): Promise { 37 | throw this.#throwTransactionsError() 38 | } 39 | 40 | async releaseConnection(): Promise { 41 | // noop 42 | } 43 | 44 | async destroy(): Promise { 45 | // noop 46 | } 47 | 48 | #createRequestHeaders(): SurrealDbHttpRequestHeaders & Record { 49 | const decodedAuth = `${this.#config.username}:${this.#config.password}` 50 | 51 | const auth = encodeToBase64(decodedAuth) 52 | 53 | return { 54 | Accept: 'application/json', 55 | Authorization: `Basic ${auth}`, 56 | DB: this.#config.database, 57 | NS: this.#config.namespace, 58 | } 59 | } 60 | 61 | #throwTransactionsError(): never { 62 | throw new SurrealDbHttpTransactionsUnsupportedError() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/dialect/http/http-errors.ts: -------------------------------------------------------------------------------- 1 | export class SurrealDbHttpTransactionsUnsupportedError extends Error { 2 | constructor() { 3 | super('SurrealDB HTTP endpoints do not support transactions!') 4 | this.name = 'SurrealDbHttpTransactionsUnsupportedError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/dialect/http/http-introspector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sql, 3 | type DatabaseIntrospector, 4 | type DatabaseMetadata, 5 | type DatabaseMetadataOptions, 6 | type Kysely, 7 | type SchemaMetadata, 8 | type TableMetadata, 9 | } from 'kysely' 10 | 11 | import {SurrealDbSchemasUnsupportedError} from '../errors.js' 12 | import type { 13 | SurrealDbHttpInfoForDbResponseBodyItemResult, 14 | SurrealDbHttpInfoForTableResponseBodyItemResult, 15 | SurrealDbHttpResponseBodyItem, 16 | } from './http-types.js' 17 | 18 | export class SurrealDbHttpIntrospector implements DatabaseIntrospector { 19 | readonly #db: Kysely 20 | 21 | constructor(db: Kysely) { 22 | this.#db = db 23 | } 24 | 25 | async getSchemas(): Promise { 26 | throw new SurrealDbSchemasUnsupportedError() 27 | } 28 | 29 | async getTables(): Promise { 30 | const infoForDb = await this.#requestInfoFor('db') 31 | 32 | return await Promise.all( 33 | Object.keys(infoForDb.tb).map(async (tableName) => { 34 | const infoForTable = await this.#requestInfoFor('table', tableName) 35 | 36 | return { 37 | columns: Object.entries(infoForTable.fd).map(([name, definition]) => ({ 38 | dataType: this.#extractDataTypeFromFieldDefinition(definition), 39 | hasDefaultValue: false, 40 | isAutoIncrementing: false, 41 | isNullable: true, 42 | name, 43 | })), 44 | isView: false, 45 | name: tableName, 46 | } 47 | }), 48 | ) 49 | } 50 | 51 | async getMetadata(_?: DatabaseMetadataOptions | undefined): Promise { 52 | return {tables: await this.getTables()} 53 | } 54 | 55 | async #requestInfoFor< 56 | E extends RequestInfoForEntity, 57 | R = E extends 'db' ? SurrealDbHttpInfoForDbResponseBodyItemResult : SurrealDbHttpInfoForTableResponseBodyItemResult, 58 | I extends SurrealDbHttpResponseBodyItem = SurrealDbHttpResponseBodyItem, 59 | >(entity: E, name?: string): Promise { 60 | try { 61 | const { 62 | rows: [{result, status}], 63 | } = await sql`info for ${sql.raw(entity)}${sql.raw(name ? ` ${name}` : '')}`.execute(this.#db) 64 | 65 | if (status !== 'OK') { 66 | throw new SurrealDbHttpIntrospectorError({entity, name, reason: status}) 67 | } 68 | 69 | return result 70 | } catch (error: unknown) { 71 | const reason = error instanceof Error ? error.message : typeof error === 'string' ? error : undefined 72 | 73 | throw new SurrealDbHttpIntrospectorError({entity, name, reason}) 74 | } 75 | } 76 | 77 | #extractDataTypeFromFieldDefinition(definition: string): string { 78 | return definition.replace(/.*TYPE (\w+|`\w+`)$/, '$1') 79 | } 80 | } 81 | 82 | type RequestInfoForEntity = 'kv' | 'db' | 'ns' | 'table' 83 | 84 | export class SurrealDbHttpIntrospectorError extends Error { 85 | constructor(incident: {entity: RequestInfoForEntity; name?: string; reason?: string}) { 86 | super( 87 | `Failed getting info for ${incident.entity}${incident.name ? `:${incident.name}` : ''}${ 88 | incident.reason ? ` - ${incident.reason}` : '' 89 | }!`, 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/dialect/http/http-types.ts: -------------------------------------------------------------------------------- 1 | export interface SurrealDbHttpDialectConfig { 2 | /** 3 | * SurrealDB database name. 4 | */ 5 | database: string 6 | 7 | /** 8 | * Fetch function used to communicate with the API. 9 | * 10 | * For browsers and `node 18.x` and above, pass built-in `fetch`. 11 | * 12 | * For `node 16.x` and above, pass `undici` or `node-fetch`. 13 | * 14 | * For `node 14.x` and below, pass `node-fetch`. 15 | */ 16 | fetch: (input: string, init?: FetchRequest) => Promise 17 | 18 | /** 19 | * SurrealDB cluster hostname. 20 | */ 21 | hostname: string 22 | 23 | /** 24 | * SurrealDB database namespace. 25 | */ 26 | namespace: string 27 | 28 | /** 29 | * SurrealDB database password. 30 | */ 31 | password: string 32 | 33 | /** 34 | * SurrealDB database username. 35 | */ 36 | username: string 37 | } 38 | 39 | export interface FetchRequest { 40 | method: 'POST' 41 | headers: Record 42 | body: string 43 | } 44 | 45 | export interface FetchResponse { 46 | ok: boolean 47 | status: number 48 | statusText: string 49 | json: () => Promise 50 | text: () => Promise 51 | } 52 | 53 | /** 54 | * @see https://surrealdb.com/docs/integration/http#sql 55 | */ 56 | export interface SurrealDbHttpRequestHeaders extends Record { 57 | Accept: 'application/json' 58 | Authorization: `Basic ${string}` 59 | DB: string 60 | NS: string 61 | } 62 | 63 | export type SurrealDbHttpResponseBody = SurrealDbHttpResponseBodyItem[] 64 | 65 | export type SurrealDbHttpResponseBodyItem = 66 | | { 67 | result: R 68 | status: 'OK' 69 | time: string 70 | } 71 | | { 72 | detail: string 73 | result: never 74 | status: 'ERR' 75 | time: string 76 | } 77 | 78 | export type SurrealDbHttpDmlResponseBodyItem = SurrealDbHttpResponseBodyItem 79 | 80 | export type SurrealDbHttpDdlResponseBodyItem = SurrealDbHttpResponseBodyItem 81 | 82 | export type SurrealDbHttpInfoForDbReponseBodyItem = 83 | SurrealDbHttpResponseBodyItem 84 | 85 | export interface SurrealDbHttpInfoForDbResponseBodyItemResult { 86 | dl: Record 87 | dt: Record 88 | sc: Record 89 | tb: Record 90 | } 91 | 92 | export type SurrealDbHttpInfoForTableResposeBodyItem = 93 | SurrealDbHttpResponseBodyItem 94 | 95 | export interface SurrealDbHttpInfoForTableResponseBodyItemResult { 96 | ev: Record 97 | fd: Record 98 | ft: Record 99 | ix: Record 100 | } 101 | -------------------------------------------------------------------------------- /src/dialect/shared.ts: -------------------------------------------------------------------------------- 1 | import type {CompiledQuery} from 'kysely' 2 | 3 | export function resolveBasePath(hostname: string): string { 4 | const protocol = hostname.startsWith('localhost') || hostname.startsWith('127.0.0.1') ? 'http' : 'https' 5 | 6 | return `${protocol}://${hostname}` 7 | } 8 | 9 | export function serializeQuery(compiledQuery: CompiledQuery): string { 10 | const {parameters, sql} = compiledQuery 11 | 12 | if (!parameters.length) { 13 | return `${sql};` 14 | } 15 | 16 | return ( 17 | [ 18 | ...parameters.map( 19 | (parameter, index) => 20 | `let $${index + 1} = ${ 21 | typeof parameter === 'string' && parameter.startsWith('SURREALQL::') 22 | ? parameter.replace(/^SURREALQL::(\(.+\))/, '$1') 23 | : JSON.stringify(parameter) 24 | }`, 25 | ), 26 | sql, 27 | '', 28 | ].join(';') + ';' 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/dialect/websockets/websockets-connection.ts: -------------------------------------------------------------------------------- 1 | import type {CompiledQuery, DatabaseConnection, QueryResult} from 'kysely' 2 | import type {Surreal} from 'surrealdb.js' 3 | 4 | import {assertSingleStatementQuery, SurrealDbDatabaseError, SurrealDbStreamingUnsupportedError} from '../errors.js' 5 | import {resolveBasePath, serializeQuery} from '../shared.js' 6 | import type {SurrealDbJsQueryResult, SurrealDbWebSocketsDialectConfig} from './websockets-types.js' 7 | 8 | export class SurrealDbWebSocketsConnection implements DatabaseConnection { 9 | readonly #config: SurrealDbWebSocketsDialectConfig 10 | #driver: Surreal | undefined 11 | 12 | constructor(config: SurrealDbWebSocketsDialectConfig) { 13 | this.#config = config 14 | } 15 | 16 | close(): this { 17 | this.#driver?.close() 18 | 19 | return this 20 | } 21 | 22 | async connect(): Promise { 23 | if (this.#driver) { 24 | return this 25 | } 26 | 27 | this.#driver = new this.#config.Driver() 28 | 29 | const basePath = resolveBasePath(this.#config.hostname) 30 | 31 | this.#driver.connect(`${basePath}/rpc`) 32 | 33 | await ('token' in this.#config 34 | ? this.#driver.authenticate(this.#config.token) 35 | : this.#driver.signin({ 36 | pass: this.#config.password, 37 | SC: this.#config.scope, 38 | user: this.#config.username, 39 | })) 40 | 41 | await this.#driver.use(this.#config.namespace, this.#config.database) 42 | 43 | return this 44 | } 45 | 46 | async executeQuery(compiledQuery: CompiledQuery): Promise> { 47 | if (!this.#driver) { 48 | throw new Error('Driver not initialized!') 49 | } 50 | 51 | assertSingleStatementQuery(compiledQuery) 52 | 53 | const query = serializeQuery(compiledQuery) 54 | 55 | const results = await this.#driver.query(query) 56 | 57 | const rows = this.#extractRows(results) 58 | 59 | return { 60 | numAffectedRows: BigInt(rows.length), 61 | rows, 62 | } 63 | } 64 | 65 | async *streamQuery(_: CompiledQuery): AsyncIterableIterator> { 66 | throw new SurrealDbStreamingUnsupportedError() 67 | } 68 | 69 | #extractRows(results: SurrealDbJsQueryResult[]): R[] { 70 | const result = results[results.length - 1] 71 | 72 | if (!result) { 73 | return [] 74 | } 75 | 76 | if (!('status' in result)) { 77 | throw new SurrealDbDatabaseError(JSON.stringify(result)) 78 | } 79 | 80 | if (result.status !== 'OK') { 81 | throw new SurrealDbDatabaseError(result.detail) 82 | } 83 | 84 | const {result: rows} = result 85 | 86 | return Array.isArray(rows) ? rows : [] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/dialect/websockets/websockets-dialect.ts: -------------------------------------------------------------------------------- 1 | import type {DatabaseIntrospector, Dialect, DialectAdapter, Driver, Kysely, QueryCompiler} from 'kysely' 2 | 3 | import {SurrealDbQueryCompiler} from '../../query-compiler/query-compiler.js' 4 | import {SurrealDbAdapter} from '../adapter.js' 5 | import {SurrealDbWebSocketsDriver} from './websockets-driver.js' 6 | import {SurrealDbWebSocketsIntrospector} from './websockets-introspector.js' 7 | import type {SurrealDbWebSocketsDialectConfig} from './websockets-types.js' 8 | 9 | export class SurrealDbWebSocketsDialect implements Dialect { 10 | readonly #config: SurrealDbWebSocketsDialectConfig 11 | 12 | constructor(config: SurrealDbWebSocketsDialectConfig) { 13 | this.#config = config 14 | } 15 | 16 | createAdapter(): DialectAdapter { 17 | return new SurrealDbAdapter() 18 | } 19 | 20 | createDriver(): Driver { 21 | return new SurrealDbWebSocketsDriver(this.#config) 22 | } 23 | 24 | createIntrospector(db: Kysely): DatabaseIntrospector { 25 | return new SurrealDbWebSocketsIntrospector(db) 26 | } 27 | 28 | createQueryCompiler(): QueryCompiler { 29 | return new SurrealDbQueryCompiler() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/dialect/websockets/websockets-driver.ts: -------------------------------------------------------------------------------- 1 | import {CompiledQuery, type DatabaseConnection, type Driver} from 'kysely' 2 | 3 | import {SurrealDbWebSocketsConnection} from './websockets-connection.js' 4 | import type {SurrealDbWebSocketsDialectConfig} from './websockets-types.js' 5 | 6 | export class SurrealDbWebSocketsDriver implements Driver { 7 | readonly #config: SurrealDbWebSocketsDialectConfig 8 | #connection: SurrealDbWebSocketsConnection | undefined 9 | 10 | constructor(config: SurrealDbWebSocketsDialectConfig) { 11 | this.#config = config 12 | } 13 | 14 | async init(): Promise { 15 | // noop 16 | } 17 | 18 | async acquireConnection(): Promise { 19 | return (this.#connection ||= await this.#connect()) 20 | } 21 | 22 | async beginTransaction(connection: SurrealDbWebSocketsConnection): Promise { 23 | // swap existing non-transactional connection for a new one, 24 | // use the old one for the transaction 25 | this.#connection = await this.#connect() 26 | 27 | await connection.executeQuery(CompiledQuery.raw('begin transaction')) 28 | } 29 | 30 | async commitTransaction(connection: SurrealDbWebSocketsConnection): Promise { 31 | await connection.executeQuery(CompiledQuery.raw('commit transaction')) 32 | 33 | connection.close() 34 | } 35 | 36 | async rollbackTransaction(connection: SurrealDbWebSocketsConnection): Promise { 37 | await connection.executeQuery(CompiledQuery.raw('cancel transaction')) 38 | 39 | connection.close() 40 | } 41 | 42 | async releaseConnection(): Promise { 43 | // noop 44 | } 45 | 46 | async destroy(): Promise { 47 | this.#connection?.close() 48 | } 49 | 50 | #connect() { 51 | return new SurrealDbWebSocketsConnection(this.#config).connect() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/dialect/websockets/websockets-introspector.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DatabaseIntrospector, 3 | DatabaseMetadata, 4 | DatabaseMetadataOptions, 5 | Kysely, 6 | SchemaMetadata, 7 | TableMetadata, 8 | } from 'kysely' 9 | 10 | import {SurrealDbSchemasUnsupportedError} from '../errors.js' 11 | 12 | export class SurrealDbWebSocketsIntrospector implements DatabaseIntrospector { 13 | readonly #db: Kysely 14 | 15 | constructor(db: Kysely) { 16 | this.#db = db 17 | } 18 | 19 | async getSchemas(): Promise { 20 | throw new SurrealDbSchemasUnsupportedError() 21 | } 22 | 23 | async getTables(): Promise { 24 | throw new Error('Unimplemented!') 25 | } 26 | 27 | async getMetadata(_?: DatabaseMetadataOptions | undefined): Promise { 28 | return {tables: await this.getTables()} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/dialect/websockets/websockets-types.ts: -------------------------------------------------------------------------------- 1 | import type Surreal from 'surrealdb.js' 2 | 3 | export type SurrealDbWebSocketsDialectConfig = SurrealDbWebSocketsDialectConfigBase & 4 | (SurrealDbWebSocketsDialectConfigCredentials | SurrealDbWebSocketsDialectConfigToken) 5 | 6 | export interface SurrealDbWebSocketsDialectConfigBase { 7 | /** 8 | * SurrealDB database name. 9 | */ 10 | database: string 11 | 12 | /** 13 | * SurrealDB JavaScript driver class. 14 | */ 15 | Driver: typeof Surreal 16 | 17 | /** 18 | * SurrealDB cluster hostname. 19 | */ 20 | hostname: string 21 | 22 | /** 23 | * SurrealDB database namespace. 24 | */ 25 | namespace: string 26 | } 27 | 28 | export interface SurrealDbWebSocketsDialectConfigCredentials { 29 | /** 30 | * SurrealDB password. 31 | */ 32 | password: string 33 | 34 | /** 35 | * SurrealDB authentication scope. 36 | */ 37 | scope?: string 38 | 39 | /** 40 | * SurrealDB username. 41 | */ 42 | username: string 43 | } 44 | 45 | export interface SurrealDbWebSocketsDialectConfigToken { 46 | /** 47 | * SurrealDB jwt token. 48 | */ 49 | token: string 50 | } 51 | 52 | export type SurrealDbJsQueryResult = SurrealDbJsQueryResultOk | SurrealDbJsQueryResultErr 53 | 54 | export type SurrealDbJsQueryResultOk = { 55 | status: 'OK' 56 | time: string 57 | result: T 58 | detail?: never 59 | } 60 | 61 | export type SurrealDbJsQueryResultErr = { 62 | status: 'ERR' 63 | time: string 64 | result?: never 65 | detail: string 66 | } 67 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import {sql} from 'kysely' 2 | 3 | export const FALSE = sql`false` 4 | 5 | export const NONE = sql`none` 6 | 7 | export const NULL = sql`null` 8 | 9 | export const TRUE = sql`true` 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dialect/adapter.js' 2 | export * from './dialect/errors.js' 3 | export * from './dialect/http/http-dialect.js' 4 | export * from './dialect/http/http-driver.js' 5 | export * from './dialect/http/http-errors.js' 6 | export * from './dialect/http/http-introspector.js' 7 | export type {FetchRequest, FetchResponse, SurrealDbHttpDialectConfig} from './dialect/http/http-types.js' 8 | export * from './dialect/websockets/websockets-dialect.js' 9 | export * from './dialect/websockets/websockets-driver.js' 10 | export * from './dialect/websockets/websockets-introspector.js' 11 | export type {SurrealDbWebSocketsDialectConfig} from './dialect/websockets/websockets-types.js' 12 | export * from './query-builder/create-query-builder.js' 13 | export * from './query-builder/if-else-query-builder.js' 14 | export * from './query-builder/relate-query-builder.js' 15 | export * from './query-compiler/query-compiler.js' 16 | export * from './surreal-kysely.js' 17 | export * from './util/surreal-types.js' 18 | -------------------------------------------------------------------------------- /src/operation-node/create-query-node.ts: -------------------------------------------------------------------------------- 1 | import type {ColumnUpdateNode, TableNode, ValueNode} from 'kysely' 2 | 3 | import {freeze} from '../util/object-utils.js' 4 | import type {SurrealOperationNode} from './operation-node.js' 5 | import type {ReturnNode} from './return-node.js' 6 | 7 | export interface CreateQueryNode extends SurrealOperationNode { 8 | readonly kind: 'CreateQueryNode' 9 | readonly target: TableNode 10 | readonly content?: ValueNode 11 | readonly set?: ReadonlyArray 12 | readonly return?: ReturnNode 13 | } 14 | 15 | /** 16 | * @internal 17 | */ 18 | export const CreateQueryNode = freeze({ 19 | is(node: SurrealOperationNode): node is CreateQueryNode { 20 | return node.kind === 'CreateQueryNode' 21 | }, 22 | 23 | create(target: TableNode): CreateQueryNode { 24 | return freeze({ 25 | kind: 'CreateQueryNode', 26 | target, 27 | }) 28 | }, 29 | 30 | cloneWithContent(createQuery: CreateQueryNode, content: ValueNode): CreateQueryNode { 31 | return freeze({ 32 | ...createQuery, 33 | content, 34 | }) 35 | }, 36 | 37 | cloneWithSet(createQuery: CreateQueryNode, set: ReadonlyArray): CreateQueryNode { 38 | return freeze({ 39 | ...createQuery, 40 | set: createQuery.set ? freeze([...createQuery.set, ...set]) : set, 41 | }) 42 | }, 43 | 44 | cloneWithReturn(createQuery: CreateQueryNode, returnNode: ReturnNode): CreateQueryNode { 45 | return freeze({ 46 | ...createQuery, 47 | return: returnNode, 48 | }) 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /src/operation-node/else-if-node.ts: -------------------------------------------------------------------------------- 1 | import type {OperationNode} from 'kysely' 2 | 3 | import {freeze} from '../util/object-utils.js' 4 | import type {SurrealOperationNode} from './operation-node.js' 5 | 6 | export interface ElseIfNode extends SurrealOperationNode { 7 | readonly kind: 'ElseIfNode' 8 | readonly if: OperationNode 9 | readonly then: OperationNode 10 | } 11 | 12 | /** 13 | * @internal 14 | */ 15 | export const ElseIfNode = freeze({ 16 | is(node: SurrealOperationNode): node is ElseIfNode { 17 | return node.kind === 'ElseIfNode' 18 | }, 19 | 20 | create(condition: OperationNode, expression: OperationNode): ElseIfNode { 21 | return freeze({ 22 | kind: 'ElseIfNode', 23 | if: condition, 24 | then: expression, 25 | }) 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/operation-node/if-else-query-node.ts: -------------------------------------------------------------------------------- 1 | import type {OperationNode} from 'kysely' 2 | 3 | import {freeze} from '../util/object-utils.js' 4 | import {ElseIfNode} from './else-if-node.js' 5 | import type {SurrealOperationNode} from './operation-node.js' 6 | 7 | export interface IfElseQueryNode extends SurrealOperationNode { 8 | readonly kind: 'IfElseQueryNode' 9 | readonly if: OperationNode 10 | readonly then: OperationNode 11 | readonly else?: OperationNode 12 | readonly elseIf?: ReadonlyArray 13 | } 14 | 15 | /** 16 | * @internal 17 | */ 18 | export const IfElseQueryNode = freeze({ 19 | is(node: SurrealOperationNode): node is IfElseQueryNode { 20 | return node.kind === 'IfElseQueryNode' 21 | }, 22 | 23 | create(condition: OperationNode, expression: OperationNode): IfElseQueryNode { 24 | return freeze({ 25 | kind: 'IfElseQueryNode', 26 | if: condition, 27 | then: expression, 28 | }) 29 | }, 30 | 31 | cloneWithElse(ifElseQuery: IfElseQueryNode, elze: OperationNode): IfElseQueryNode { 32 | return freeze({ 33 | ...ifElseQuery, 34 | else: elze, 35 | }) 36 | }, 37 | 38 | cloneWithElseIf(ifElseQuery: IfElseQueryNode, condition: OperationNode, expression: OperationNode): IfElseQueryNode { 39 | const elseIfNode = ElseIfNode.create(condition, expression) 40 | 41 | return freeze({ 42 | ...ifElseQuery, 43 | elseIf: ifElseQuery.elseIf !== undefined ? [...ifElseQuery.elseIf, elseIfNode] : [elseIfNode], 44 | }) 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /src/operation-node/operation-node.ts: -------------------------------------------------------------------------------- 1 | export type SurrealOperationNodeKind = 2 | | 'CreateQueryNode' 3 | | 'ElseIfNode' 4 | | 'IfElseQueryNode' 5 | | 'RelateQueryNode' 6 | | 'ReturnNode' 7 | | 'VertexNode' 8 | 9 | export interface SurrealOperationNode { 10 | readonly kind: SurrealOperationNodeKind 11 | } 12 | 13 | const surrealKindDictionary: Record = { 14 | CreateQueryNode: true, 15 | ElseIfNode: true, 16 | IfElseQueryNode: true, 17 | RelateQueryNode: true, 18 | ReturnNode: true, 19 | VertexNode: true, 20 | } 21 | 22 | export function isSurrealOperationNode(node: {kind: string}): node is SurrealOperationNode { 23 | return surrealKindDictionary[node.kind as keyof typeof surrealKindDictionary] === true 24 | } 25 | -------------------------------------------------------------------------------- /src/operation-node/relate-query-node.ts: -------------------------------------------------------------------------------- 1 | import type {ColumnUpdateNode, TableNode, ValueNode} from 'kysely' 2 | 3 | import {freeze} from '../util/object-utils.js' 4 | import type {SurrealOperationNode} from './operation-node.js' 5 | import type {ReturnNode} from './return-node.js' 6 | import type {VertexNode} from './vertex-node.js' 7 | 8 | export interface RelateQueryNode extends SurrealOperationNode { 9 | readonly kind: 'RelateQueryNode' 10 | readonly from?: VertexNode 11 | readonly edge: TableNode 12 | readonly to?: VertexNode 13 | readonly content?: ValueNode 14 | readonly set?: ReadonlyArray 15 | readonly return?: ReturnNode 16 | } 17 | 18 | /** 19 | * @internal 20 | */ 21 | export const RelateQueryNode = freeze({ 22 | is(node: SurrealOperationNode): node is RelateQueryNode { 23 | return node.kind === 'RelateQueryNode' 24 | }, 25 | 26 | create(edge: TableNode): RelateQueryNode { 27 | return freeze({ 28 | kind: 'RelateQueryNode', 29 | edge, 30 | }) 31 | }, 32 | 33 | cloneWithFrom(relateQuery: RelateQueryNode, from: VertexNode): RelateQueryNode { 34 | return freeze({ 35 | ...relateQuery, 36 | from, 37 | }) 38 | }, 39 | 40 | cloneWithTo(relateQuery: RelateQueryNode, to: VertexNode): RelateQueryNode { 41 | return freeze({ 42 | ...relateQuery, 43 | to, 44 | }) 45 | }, 46 | 47 | cloneWithContent(relateQuery: RelateQueryNode, content: ValueNode): RelateQueryNode { 48 | return freeze({ 49 | ...relateQuery, 50 | content, 51 | }) 52 | }, 53 | 54 | cloneWithSet(relateQuery: RelateQueryNode, set: ReadonlyArray): RelateQueryNode { 55 | return freeze({ 56 | ...relateQuery, 57 | set: relateQuery.set ? freeze([...relateQuery.set, ...set]) : set, 58 | }) 59 | }, 60 | 61 | cloneWithReturn(relateQuery: RelateQueryNode, returnNode: ReturnNode): RelateQueryNode { 62 | return freeze({ 63 | ...relateQuery, 64 | return: returnNode, 65 | }) 66 | }, 67 | }) 68 | -------------------------------------------------------------------------------- /src/operation-node/return-node.ts: -------------------------------------------------------------------------------- 1 | import type {ColumnNode} from 'kysely' 2 | 3 | import {freeze} from '../util/object-utils.js' 4 | import type {SurrealOperationNode} from './operation-node.js' 5 | 6 | export type SurrealReturnType = 'none' | 'before' | 'after' | 'diff' 7 | 8 | export interface ReturnNode extends SurrealOperationNode { 9 | readonly kind: 'ReturnNode' 10 | readonly return: SurrealReturnType | ReadonlyArray 11 | } 12 | 13 | /** 14 | * @internal 15 | */ 16 | export const ReturnNode = freeze({ 17 | is(node: SurrealOperationNode): node is ReturnNode { 18 | return node.kind === 'ReturnNode' 19 | }, 20 | 21 | create(returned: SurrealReturnType | ReadonlyArray): ReturnNode { 22 | return freeze({ 23 | kind: 'ReturnNode', 24 | return: returned, 25 | }) 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/operation-node/vertex-node.ts: -------------------------------------------------------------------------------- 1 | import type {RawNode, SelectQueryNode, TableNode} from 'kysely' 2 | 3 | import {freeze} from '../util/object-utils.js' 4 | import type {SurrealOperationNode} from './operation-node.js' 5 | 6 | export type VertexExpressionNode = TableNode | ReadonlyArray | RawNode | SelectQueryNode 7 | 8 | export interface VertexNode extends SurrealOperationNode { 9 | readonly kind: 'VertexNode' 10 | readonly vertex: VertexExpressionNode 11 | } 12 | 13 | /** 14 | * @internal 15 | */ 16 | export const VertexNode = freeze({ 17 | is(node: SurrealOperationNode): node is VertexNode { 18 | return node.kind === 'VertexNode' 19 | }, 20 | 21 | create(vertex: VertexExpressionNode): VertexNode { 22 | return freeze({ 23 | kind: 'VertexNode', 24 | vertex, 25 | }) 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/parser/create-object-parser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ColumnNode, 3 | ColumnUpdateNode, 4 | isOperationNodeSource, 5 | ValueNode, 6 | type InsertObject, 7 | type OperationNode, 8 | } from 'kysely' 9 | 10 | export type CreateObject = InsertObject 11 | 12 | export function parseSetObject(row: CreateObject): ReadonlyArray { 13 | return Object.entries(row) 14 | .filter(([_, value]) => value !== undefined) 15 | .map(([key, value]) => ColumnUpdateNode.create(ColumnNode.create(key), parseSetObjectValue(value))) 16 | } 17 | 18 | function parseSetObjectValue(value: unknown): OperationNode { 19 | if (isOperationNodeSource(value)) { 20 | return value.toOperationNode() as any 21 | } 22 | 23 | // TODO: handle nested raw builders. 24 | 25 | return ValueNode.create(value) 26 | } 27 | 28 | export function parseContent(row: CreateObject): ValueNode { 29 | if (isOperationNodeSource(row)) { 30 | return row.toOperationNode() as any 31 | } 32 | 33 | // TODO: handle nested raw builders. 34 | 35 | return ValueNode.create(row) 36 | } 37 | -------------------------------------------------------------------------------- /src/parser/return-parser.ts: -------------------------------------------------------------------------------- 1 | import {ColumnNode, type AnyColumn, type SelectType} from 'kysely' 2 | 3 | import {ReturnNode, type SurrealReturnType} from '../operation-node/return-node.js' 4 | 5 | export type ReturnExpression = 6 | | SurrealReturnType 7 | | AnyColumn 8 | | ReadonlyArray> 9 | 10 | export type ExtractTypeFromReturnExpression< 11 | DB, 12 | TB extends keyof DB, 13 | RE extends ReturnExpression, 14 | O = DB[TB], 15 | > = RE extends 'none' 16 | ? never 17 | : RE extends Exclude 18 | ? O 19 | : RE extends AnyColumn 20 | ? O extends DB[TB] 21 | ? {[K in RE]: SelectType>} 22 | : O & {[K in RE]: SelectType>} 23 | : RE extends ReadonlyArray> 24 | ? O extends DB[TB] 25 | ? {[K in RE[number]]: SelectType>} 26 | : O & {[K in RE[number]]: SelectType>} 27 | : unknown 28 | 29 | type ExtractColumnType = { 30 | [T in TB]: C extends keyof DB[T] ? DB[T][C] : never 31 | }[TB] 32 | 33 | export function parseReturnExpression(expression: ReturnExpression): ReturnNode { 34 | if (isSurrealReturnType(expression)) { 35 | return ReturnNode.create(expression) 36 | } 37 | 38 | if (!Array.isArray(expression)) { 39 | expression = [expression] as ReadonlyArray 40 | } 41 | 42 | return ReturnNode.create(expression.map(ColumnNode.create)) 43 | } 44 | 45 | export function isSurrealReturnType(expression: unknown): expression is SurrealReturnType { 46 | return expression === 'none' || expression === 'before' || expression === 'after' || expression === 'diff' 47 | } 48 | -------------------------------------------------------------------------------- /src/parser/vertex-expression-parser.ts: -------------------------------------------------------------------------------- 1 | import {TableNode, type AnySelectQueryBuilder, type RawBuilder} from 'kysely' 2 | 3 | import {VertexNode} from '../operation-node/vertex-node.js' 4 | import {isReadonlyArray} from '../util/object-utils.js' 5 | import type {AnySpecificVertex} from '../util/surreal-types.js' 6 | 7 | export type VertexExpression = 8 | | AnySpecificVertex 9 | | ReadonlyArray> 10 | | AnySelectQueryBuilder 11 | | RawBuilder 12 | 13 | export function parseVertexExpression(expression: VertexExpression): VertexNode { 14 | if (typeof expression === 'string') { 15 | return VertexNode.create(TableNode.create(expression)) 16 | } 17 | 18 | if (isReadonlyArray(expression)) { 19 | return VertexNode.create(expression.map(TableNode.create)) 20 | } 21 | 22 | return VertexNode.create(expression.toOperationNode()) 23 | } 24 | -------------------------------------------------------------------------------- /src/query-builder/create-query-builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NoResultError, 3 | type Compilable, 4 | type CompiledQuery, 5 | type KyselyPlugin, 6 | type NoResultErrorConstructor, 7 | type QueryExecutor, 8 | } from 'kysely' 9 | 10 | import {CreateQueryNode} from '../operation-node/create-query-node.js' 11 | import {parseContent, parseSetObject, type CreateObject} from '../parser/create-object-parser.js' 12 | import { 13 | parseReturnExpression, 14 | type ExtractTypeFromReturnExpression, 15 | type ReturnExpression, 16 | } from '../parser/return-parser.js' 17 | import {preventAwait} from '../util/prevent-await.js' 18 | import type {QueryId} from '../util/query-id.js' 19 | import type {MergePartial} from '../util/type-utils.js' 20 | import type {ReturnInterface} from './return-interface.js' 21 | import type {SetContentInterface} from './set-content-interface.js' 22 | 23 | export class CreateQueryBuilder 24 | implements Compilable, ReturnInterface, SetContentInterface 25 | { 26 | readonly #props: CreateQueryBuilderProps 27 | 28 | constructor(props: CreateQueryBuilderProps) { 29 | this.#props = props 30 | } 31 | 32 | content>(content: C): CreateQueryBuilder { 33 | return new CreateQueryBuilder({ 34 | ...this.#props, 35 | queryNode: CreateQueryNode.cloneWithContent(this.#props.queryNode, parseContent(content)), 36 | }) 37 | } 38 | 39 | set>(values: V): CreateQueryBuilder { 40 | return new CreateQueryBuilder({ 41 | ...this.#props, 42 | queryNode: CreateQueryNode.cloneWithSet(this.#props.queryNode, parseSetObject(values)), 43 | }) 44 | } 45 | 46 | return>( 47 | expression: RE, 48 | ): CreateQueryBuilder> { 49 | return new CreateQueryBuilder({ 50 | ...this.#props, 51 | queryNode: CreateQueryNode.cloneWithReturn(this.#props.queryNode, parseReturnExpression(expression)), 52 | }) 53 | } 54 | 55 | /** 56 | * Simply calls the given function passing `this` as the only argument. 57 | * 58 | * If you want to conditionally call a method on `this`, see the {@link if} method. 59 | * 60 | * ### Examples 61 | * 62 | * The next example uses a helper funtion `log` to log a query: 63 | * 64 | * ```ts 65 | * function log(qb: T): T { 66 | * console.log(qb.compile()) 67 | * return qb 68 | * } 69 | * 70 | * db.updateTable('person') 71 | * .set(values) 72 | * .$call(log) 73 | * .execute() 74 | * ``` 75 | */ 76 | $call(func: (qb: this) => T): T { 77 | return func(this) 78 | } 79 | 80 | /** 81 | * @deprecated Use {@link $call} instead. 82 | */ 83 | call(func: (qb: this) => T): T { 84 | return this.$call(func) 85 | } 86 | 87 | /** 88 | * Call `func(this)` if `condition` is true. 89 | * 90 | * This method is especially handy with optional selects. Any `return` method 91 | * calls add columns as optional fields to the output type when called inside 92 | * the `func` callback. This is because we can't know if those selections were 93 | * actually made before running the code. 94 | * 95 | * You can also call any other methods inside the callback. 96 | * 97 | * ### Examples 98 | * 99 | * ```ts 100 | * async function createPerson(values: Insertable, returnLastName: boolean) { 101 | * return await db 102 | * .create('person') 103 | * .set(values) 104 | * .return(['id', 'first_name']) 105 | * .$if(returnLastName, (qb) => qb.return('last_name')) 106 | * .executeTakeFirstOrThrow() 107 | * } 108 | * ``` 109 | * 110 | * Any selections added inside the `if` callback will be added as optional fields to the 111 | * output type since we can't know if the selections were actually made before running 112 | * the code. In the example above the return type of the `createPerson` function is: 113 | * 114 | * ```ts 115 | * { 116 | * id: number 117 | * first_name: string 118 | * last_name?: string 119 | * } 120 | * ``` 121 | */ 122 | $if( 123 | condition: boolean, 124 | func: (qb: this) => CreateQueryBuilder, 125 | ): CreateQueryBuilder> { 126 | if (condition) { 127 | return func(this) as any 128 | } 129 | 130 | return new CreateQueryBuilder({ 131 | ...this.#props, 132 | }) 133 | } 134 | 135 | /** 136 | * @deprecated Use the {@link $if} method instead. 137 | */ 138 | if( 139 | condition: boolean, 140 | func: (qb: this) => CreateQueryBuilder, 141 | ): CreateQueryBuilder> { 142 | return this.$if(condition, func) 143 | } 144 | 145 | /** 146 | * Change the output type of the query. 147 | * 148 | * You should only use this method as the last resort if the types don't support 149 | * your use case. 150 | */ 151 | $castTo(): CreateQueryBuilder { 152 | return new CreateQueryBuilder(this.#props) 153 | } 154 | 155 | /** 156 | * @deprecated Use the {@link $castTo} method instead. 157 | */ 158 | castTo(): CreateQueryBuilder { 159 | return this.$castTo() 160 | } 161 | 162 | /** 163 | * Returns a copy of this CreateQueryBuilder instance with the given plugin installed. 164 | */ 165 | $withPlugin(plugin: KyselyPlugin): CreateQueryBuilder { 166 | return new CreateQueryBuilder({ 167 | ...this.#props, 168 | executor: this.#props.executor.withPlugin(plugin), 169 | }) 170 | } 171 | 172 | /** 173 | * @deprecated Use the {@link $withPlugin} method instead. 174 | */ 175 | withPlugin(plugin: KyselyPlugin): CreateQueryBuilder { 176 | return this.$withPlugin(plugin) 177 | } 178 | 179 | toOperationNode(): CreateQueryNode { 180 | return this.#props.executor.transformQuery(this.#props.queryNode as any, this.#props.queryId) as any 181 | } 182 | 183 | compile(): CompiledQuery { 184 | return this.#props.executor.compileQuery(this.toOperationNode() as any, this.#props.queryId) 185 | } 186 | 187 | /** 188 | * Executes the query and returns an array of rows. 189 | * 190 | * Also see the {@link executeTakeFirst} and {@link executeTakeFirstOrThrow} 191 | * methods. 192 | */ 193 | async execute(): Promise { 194 | const compiledQuery = this.compile() 195 | 196 | const result = await this.#props.executor.executeQuery(compiledQuery, this.#props.queryId) 197 | 198 | return result.rows 199 | } 200 | 201 | /** 202 | * Executes the query and returns the first result or undefined if the query 203 | * returned no result. 204 | */ 205 | async executeTakeFirst(): Promise { 206 | const [result] = await this.execute() 207 | 208 | return result 209 | } 210 | 211 | /** 212 | * Executes the query and returns the first result or throws if the query returned 213 | * no result. 214 | * 215 | * By default an instance of {@link NoResultError} is thrown, but you can provide 216 | * a custom error class as the only argument to throw a different error. 217 | */ 218 | async executeTakeFirstOrThrow(errorConstructor: NoResultErrorConstructor = NoResultError): Promise { 219 | const result = await this.executeTakeFirst() 220 | 221 | if (result === undefined) { 222 | throw new errorConstructor(this.toOperationNode() as any) 223 | } 224 | 225 | return result as O 226 | } 227 | } 228 | 229 | preventAwait( 230 | CreateQueryBuilder, 231 | "don't await CreateQueryBuilder instances directly. To execute the query you need to call `execute` or `executeTakeFirst`.", 232 | ) 233 | 234 | type CreateQueryBuilderProps = { 235 | readonly executor: QueryExecutor 236 | readonly queryId: QueryId 237 | readonly queryNode: CreateQueryNode 238 | } 239 | -------------------------------------------------------------------------------- /src/query-builder/if-else-query-builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KyselyPlugin, 3 | NoResultError, 4 | type Compilable, 5 | type CompiledQuery, 6 | type Expression, 7 | type NoResultErrorConstructor, 8 | type OperationNode, 9 | type QueryExecutor, 10 | } from 'kysely' 11 | 12 | import {IfElseQueryNode} from '../operation-node/if-else-query-node.js' 13 | import {preventAwait} from '../util/prevent-await.js' 14 | import type {QueryId} from '../util/query-id.js' 15 | 16 | export class IfElseQueryBuilder { 17 | readonly #props: IfElseQueryBuilderProps 18 | 19 | constructor(props: IfElseQueryBuilderProps) { 20 | this.#props = props 21 | } 22 | 23 | /** 24 | * Adds an `else ...` clause to the query. 25 | * 26 | * see {@link elseIfThen} and {@link end}. 27 | * 28 | * ### Examples 29 | * 30 | * ```ts 31 | * db.ifThen(sql`${scope} = ${sql.literal('admin')}`, db.selectFrom('account').selectAll()) 32 | * .elseIfThen(sql`${scope} = ${sql.literal('user')}`, sql`(select * from ${auth}.account)`) 33 | * .else(sql<[]>`[]`) 34 | * .end() 35 | * ``` 36 | * 37 | * The generated SQL: 38 | * 39 | * ```sql 40 | * if $1 = 'admin' then (select * from account) 41 | * else if $2 = 'user' then (select * from $3.account) 42 | * else [] 43 | * end 44 | * ``` 45 | */ 46 | else(expression: Expression): IfElseQueryBuilder { 47 | return new IfElseQueryBuilder({ 48 | ...this.#props, 49 | queryNode: IfElseQueryNode.cloneWithElse(this.#props.queryNode, expression.toOperationNode()), 50 | }) 51 | } 52 | 53 | /** 54 | * Adds an `else if ... then ...` clause to the query. 55 | * 56 | * see {@link else} and {@link end}. 57 | * 58 | * ### Examples 59 | * 60 | * ```ts 61 | * db.ifThen(sql`${scope} = ${sql.literal('admin')}`, db.selectFrom('account').selectAll()) 62 | * .elseIfThen(sql`${scope} = ${sql.literal('user')}`, sql`(select * from ${auth}.account)`) 63 | * .else(sql<[]>`[]`) 64 | * .end() 65 | * ``` 66 | * 67 | * The generated SQL: 68 | * 69 | * ```sql 70 | * if $1 = 'admin' then (select * from account) 71 | * else if $2 = 'user' then (select * from $3.account) 72 | * else [] 73 | * end 74 | * ``` 75 | */ 76 | elseIfThen(condition: Expression, expression: Expression): IfElseQueryBuilder { 77 | return new IfElseQueryBuilder({ 78 | ...this.#props, 79 | queryNode: IfElseQueryNode.cloneWithElseIf( 80 | this.#props.queryNode, 81 | condition.toOperationNode(), 82 | expression.toOperationNode(), 83 | ), 84 | }) 85 | } 86 | 87 | /** 88 | * Adds an `end` to the query. 89 | * 90 | * see {@link elseIfThen} and {@link else}. 91 | * 92 | * ### Examples 93 | * 94 | * ```ts 95 | * db.ifThen(sql`${scope} = ${sql.literal('admin')}`, db.selectFrom('account').selectAll()) 96 | * .elseIfThen(sql`${scope} = ${sql.literal('user')}`, sql`(select * from ${auth}.account)`) 97 | * .else(sql<[]>`[]`) 98 | * .end() 99 | * ``` 100 | * 101 | * The generated SQL: 102 | * 103 | * ```sql 104 | * if $1 = 'admin' then (select * from account) 105 | * else if $2 = 'user' then (select * from $3.account) 106 | * else [] 107 | * end 108 | * ``` 109 | */ 110 | end(): EndedIfElseQueryBuilder { 111 | return new EndedIfElseQueryBuilder({ 112 | ...this.#props, 113 | }) 114 | } 115 | 116 | /** 117 | * Simply calls the given function passing `this` as the only argument. 118 | * 119 | * If you want to conditionally call a method on `this`, see the {@link if} method. 120 | * 121 | * ### Examples 122 | * 123 | * The next example uses a helper funtion `log` to log a query: 124 | * 125 | * ```ts 126 | * function log(qb: T): T { 127 | * console.log(qb.compile()) 128 | * return qb 129 | * } 130 | * 131 | * db.updateTable('person') 132 | * .set(values) 133 | * .$call(log) 134 | * .execute() 135 | * ``` 136 | */ 137 | $call(func: (qb: this) => T): T { 138 | return func(this) 139 | } 140 | 141 | /** 142 | * Call `func(this)` if `condition` is true. 143 | * 144 | * This method is especially handy with optional selects. Any `return` method 145 | * calls add columns as optional fields to the output type when called inside 146 | * the `func` callback. This is because we can't know if those selections were 147 | * actually made before running the code. 148 | * 149 | * You can also call any other methods inside the callback. 150 | * 151 | * ### Examples 152 | * 153 | * ```ts 154 | * async function createPerson(values: Insertable, returnLastName: boolean) { 155 | * return await db 156 | * .create('person') 157 | * .set(values) 158 | * .return(['id', 'first_name']) 159 | * .$if(returnLastName, (qb) => qb.return('last_name')) 160 | * .executeTakeFirstOrThrow() 161 | * } 162 | * ``` 163 | * 164 | * Any selections added inside the `if` callback will be added as optional fields to the 165 | * output type since we can't know if the selections were actually made before running 166 | * the code. In the example above the return type of the `createPerson` function is: 167 | * 168 | * ```ts 169 | * { 170 | * id: number 171 | * first_name: string 172 | * last_name?: string 173 | * } 174 | * ``` 175 | */ 176 | $if(condition: boolean, func: (qb: this) => IfElseQueryBuilder): IfElseQueryBuilder { 177 | if (condition) { 178 | return func(this) as any 179 | } 180 | 181 | return new IfElseQueryBuilder({ 182 | ...this.#props, 183 | }) 184 | } 185 | 186 | /** 187 | * Change the output type of the query. 188 | * 189 | * You should only use this method as the last resort if the types don't support 190 | * your use case. 191 | */ 192 | $castTo(): IfElseQueryBuilder { 193 | return new IfElseQueryBuilder(this.#props) 194 | } 195 | 196 | /** 197 | * Returns a copy of this IfElseQueryBuilder instance with the given plugin installed. 198 | */ 199 | $withPlugin(plugin: KyselyPlugin): IfElseQueryBuilder { 200 | return new IfElseQueryBuilder({ 201 | ...this.#props, 202 | executor: this.#props.executor.withPlugin(plugin), 203 | }) 204 | } 205 | } 206 | 207 | preventAwait( 208 | IfElseQueryBuilder, 209 | "don't await IfElseQueryBuilder instances directly. To execute the query you need to call `execute` or `executeTakeFirst`.", 210 | ) 211 | 212 | interface IfElseQueryBuilderProps { 213 | executor: QueryExecutor 214 | queryId: QueryId 215 | queryNode: IfElseQueryNode 216 | } 217 | 218 | export class EndedIfElseQueryBuilder implements Expression, Compilable { 219 | readonly #props: IfElseQueryBuilderProps 220 | 221 | constructor(props: IfElseQueryBuilderProps) { 222 | this.#props = props 223 | } 224 | 225 | /** 226 | * @internal 227 | */ 228 | get expressionType(): O | undefined { 229 | return undefined 230 | } 231 | 232 | toOperationNode(): OperationNode { 233 | return this.#props.executor.transformQuery(this.#props.queryNode as any, this.#props.queryId) 234 | } 235 | 236 | compile(): CompiledQuery { 237 | return this.#props.executor.compileQuery(this.toOperationNode() as any, this.#props.queryId) 238 | } 239 | 240 | /** 241 | * Executes the query and returns an array of rows. 242 | * 243 | * Also see the {@link executeTakeFirst} and {@link executeTakeFirstOrThrow} 244 | * methods. 245 | */ 246 | async execute(): Promise { 247 | const compiledQuery = this.compile() 248 | 249 | const results = await this.#props.executor.executeQuery(compiledQuery, this.#props.queryId) 250 | 251 | return results.rows 252 | } 253 | 254 | /** 255 | * Executes the query and returns the first result or undefined if the query 256 | * returned no result. 257 | */ 258 | async executeTakeFirst(): Promise { 259 | const results = await this.execute() 260 | 261 | return results[0] 262 | } 263 | 264 | /** 265 | * Executes the query and returns the first result or throws if the query returned 266 | * no result. 267 | * 268 | * By default an instance of {@link NoResultError} is thrown, but you can provide 269 | * a custom error class as the only argument to throw a different error. 270 | */ 271 | async executeTakeFirstOrThrow(errorConstructor: NoResultErrorConstructor = NoResultError): Promise { 272 | const result = await this.executeTakeFirst() 273 | 274 | if (result === undefined) { 275 | throw new errorConstructor(this.toOperationNode() as any) 276 | } 277 | 278 | return result 279 | } 280 | } 281 | 282 | preventAwait( 283 | EndedIfElseQueryBuilder, 284 | "don't await EndedIfElseQueryBuilder instances directly. To execute the query you need to call `execute` or `executeTakeFirst`.", 285 | ) 286 | -------------------------------------------------------------------------------- /src/query-builder/relate-query-builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NoResultError, 3 | type AnySelectQueryBuilder, 4 | type Compilable, 5 | type CompiledQuery, 6 | type KyselyPlugin, 7 | type NoResultErrorConstructor, 8 | type QueryExecutor, 9 | type RawBuilder, 10 | } from 'kysely' 11 | 12 | import {RelateQueryNode} from '../operation-node/relate-query-node.js' 13 | import {parseContent, parseSetObject, type CreateObject} from '../parser/create-object-parser.js' 14 | import { 15 | parseReturnExpression, 16 | type ExtractTypeFromReturnExpression, 17 | type ReturnExpression, 18 | } from '../parser/return-parser.js' 19 | import {parseVertexExpression, type VertexExpression} from '../parser/vertex-expression-parser.js' 20 | import {preventAwait} from '../util/prevent-await.js' 21 | import type {QueryId} from '../util/query-id.js' 22 | import type {AnySpecificVertex, AnyVertexGroup} from '../util/surreal-types.js' 23 | import type {MergePartial} from '../util/type-utils.js' 24 | import type {ReturnInterface} from './return-interface.js' 25 | import type {SetContentInterface} from './set-content-interface.js' 26 | 27 | export class RelateQueryBuilder 28 | implements Compilable, ReturnInterface, SetContentInterface 29 | { 30 | readonly #props: RelateQueryBuilderProps 31 | 32 | constructor(props: RelateQueryBuilderProps) { 33 | this.#props = props 34 | } 35 | 36 | /** 37 | * Sets the given record/s as inbound vertex/vertices of a {@link SurrealKysely.relate | relate} query's edge. 38 | * 39 | * To set outbound vertex/vertices, see {@link from}. 40 | * 41 | * ### Examples 42 | * 43 | * ```ts 44 | * import {sql} from 'kysely' 45 | * 46 | * const relation = await db 47 | * .relate('write') 48 | * .from('user:tobie') 49 | * .to('article:surreal') 50 | * .set({ 51 | * 'time.written': sql`time::now()`, 52 | * }) 53 | * .executeTakeFirst() 54 | * ``` 55 | * 56 | * The generated SurrealQL: 57 | * 58 | * ```sql 59 | * relate user:tobie -> write -> article:surreal 60 | * set time.written = time::now(); 61 | * ``` 62 | */ 63 | from(table: AnyVertexGroup, id: string | number): RelateQueryBuilder 64 | 65 | from(record: AnySpecificVertex): RelateQueryBuilder 66 | 67 | from(records: ReadonlyArray>): RelateQueryBuilder 68 | 69 | from(expression: AnySelectQueryBuilder | RawBuilder): RelateQueryBuilder 70 | 71 | from(target: AnyVertexGroup | VertexExpression, id?: string | number): any { 72 | const expression = id !== undefined ? `${String(target)}:${id}` : target 73 | 74 | return new RelateQueryBuilder({ 75 | ...this.#props, 76 | queryNode: RelateQueryNode.cloneWithFrom(this.#props.queryNode, parseVertexExpression(expression as any)), 77 | }) 78 | } 79 | 80 | /** 81 | * Sets the given record/s as outbound vertex/vertices of a {@link SurrealKysely.relate | relate} query's edge. 82 | * 83 | * To set inbound vertex/vertices, see {@link to}. 84 | * 85 | * ### Examples: 86 | * 87 | * ```ts 88 | * import {sql} from 'kysely' 89 | * 90 | * const relation = await db 91 | * .relate('write') 92 | * .from('user:tobie') 93 | * .to('article:surreal') 94 | * .set({ 95 | * 'time.written': sql`time::now()`, 96 | * }) 97 | * .executeTakeFirst() 98 | * ``` 99 | * 100 | * The generated SurrealQL: 101 | * 102 | * ```sql 103 | * relate user:tobie -> write -> article:surreal 104 | * set time.written = time::now(); 105 | * ``` 106 | */ 107 | to(table: AnyVertexGroup, id: string | number): RelateQueryBuilder 108 | 109 | to(record: AnySpecificVertex): RelateQueryBuilder 110 | 111 | to(records: ReadonlyArray>): RelateQueryBuilder 112 | 113 | to(expression: AnySelectQueryBuilder | RawBuilder): RelateQueryBuilder 114 | 115 | to(target: AnyVertexGroup | VertexExpression, id?: string | number): any { 116 | const expression = id !== undefined ? `${String(target)}:${id}` : target 117 | 118 | return new RelateQueryBuilder({ 119 | ...this.#props, 120 | queryNode: RelateQueryNode.cloneWithTo(this.#props.queryNode, parseVertexExpression(expression as any)), 121 | }) 122 | } 123 | 124 | content>(content: C): RelateQueryBuilder { 125 | return new RelateQueryBuilder({ 126 | ...this.#props, 127 | queryNode: RelateQueryNode.cloneWithContent(this.#props.queryNode, parseContent(content)), 128 | }) 129 | } 130 | 131 | set>(values: V): RelateQueryBuilder { 132 | return new RelateQueryBuilder({ 133 | ...this.#props, 134 | queryNode: RelateQueryNode.cloneWithSet(this.#props.queryNode, parseSetObject(values)), 135 | }) 136 | } 137 | 138 | return>( 139 | expression: RE, 140 | ): RelateQueryBuilder> { 141 | return new RelateQueryBuilder({ 142 | ...this.#props, 143 | queryNode: RelateQueryNode.cloneWithReturn(this.#props.queryNode, parseReturnExpression(expression)), 144 | }) 145 | } 146 | 147 | /** 148 | * Simply calls the given function passing `this` as the only argument. 149 | * 150 | * If you want to conditionally call a method on `this`, see the {@link if} method. 151 | * 152 | * ### Examples 153 | * 154 | * The next example uses a helper funtion `log` to log a query: 155 | * 156 | * ```ts 157 | * function log(qb: T): T { 158 | * console.log(qb.compile()) 159 | * return qb 160 | * } 161 | * 162 | * db.updateTable('person') 163 | * .set(values) 164 | * .$call(log) 165 | * .execute() 166 | * ``` 167 | */ 168 | $call(func: (qb: this) => T): T { 169 | return func(this) 170 | } 171 | 172 | /** 173 | * @deprecated Use {@link $call} instead. 174 | */ 175 | call(func: (qb: this) => T): T { 176 | return this.$call(func) 177 | } 178 | 179 | /** 180 | * Call `func(this)` if `condition` is true. 181 | * 182 | * This method is especially handy with optional selects. Any `return` method 183 | * calls add columns as optional fields to the output type when called inside 184 | * the `func` callback. This is because we can't know if those selections were 185 | * actually made before running the code. 186 | * 187 | * You can also call any other methods inside the callback. 188 | * 189 | * ### Examples 190 | * 191 | * ```ts 192 | * async function createPerson(values: Insertable, returnLastName: boolean) { 193 | * return await db 194 | * .create('person') 195 | * .set(values) 196 | * .return(['id', 'first_name']) 197 | * .$if(returnLastName, (qb) => qb.return('last_name')) 198 | * .executeTakeFirstOrThrow() 199 | * } 200 | * ``` 201 | * 202 | * Any selections added inside the `if` callback will be added as optional fields to the 203 | * output type since we can't know if the selections were actually made before running 204 | * the code. In the example above the return type of the `createPerson` function is: 205 | * 206 | * ```ts 207 | * { 208 | * id: number 209 | * first_name: string 210 | * last_name?: string 211 | * } 212 | * ``` 213 | */ 214 | $if( 215 | condition: boolean, 216 | func: (qb: this) => RelateQueryBuilder, 217 | ): RelateQueryBuilder> { 218 | if (condition) { 219 | return func(this) as any 220 | } 221 | 222 | return new RelateQueryBuilder({ 223 | ...this.#props, 224 | }) 225 | } 226 | 227 | /** 228 | * @deprecated Use {@link $if} instead. 229 | */ 230 | if( 231 | condition: boolean, 232 | func: (qb: this) => RelateQueryBuilder, 233 | ): RelateQueryBuilder> { 234 | return this.$if(condition, func) 235 | } 236 | 237 | /** 238 | * Change the output type of the query. 239 | * 240 | * You should only use this method as the last resort if the types don't support 241 | * your use case. 242 | */ 243 | $castTo(): RelateQueryBuilder { 244 | return new RelateQueryBuilder(this.#props) 245 | } 246 | 247 | /** 248 | * @deprecated Use {@link $castTo} instead. 249 | */ 250 | castTo(): RelateQueryBuilder { 251 | return this.$castTo() 252 | } 253 | 254 | /** 255 | * Returns a copy of this RelateQueryBuilder instance with the given plugin installed. 256 | */ 257 | $withPlugin(plugin: KyselyPlugin): RelateQueryBuilder { 258 | return new RelateQueryBuilder({ 259 | ...this.#props, 260 | executor: this.#props.executor.withPlugin(plugin), 261 | }) 262 | } 263 | 264 | /** 265 | * @deprecated Use {@link $withPlugin} instead. 266 | */ 267 | withPlugin(plugin: KyselyPlugin): RelateQueryBuilder { 268 | return this.$withPlugin(plugin) 269 | } 270 | 271 | toOperationNode(): RelateQueryNode { 272 | return this.#props.executor.transformQuery(this.#props.queryNode as any, this.#props.queryId) as any 273 | } 274 | 275 | compile(): CompiledQuery { 276 | return this.#props.executor.compileQuery(this.toOperationNode() as any, this.#props.queryId) 277 | } 278 | 279 | /** 280 | * Executes the query and returns an array of rows. 281 | * 282 | * Also see the {@link executeTakeFirst} and {@link executeTakeFirstOrThrow} 283 | * methods. 284 | */ 285 | async execute(): Promise { 286 | const compiledQuery = this.compile() 287 | 288 | const result = await this.#props.executor.executeQuery(compiledQuery, this.#props.queryId) 289 | 290 | return result.rows 291 | } 292 | 293 | /** 294 | * Executes the query and returns the first result or undefined if the query 295 | * returned no result. 296 | */ 297 | async executeTakeFirst(): Promise { 298 | const [result] = await this.execute() 299 | 300 | return result 301 | } 302 | 303 | /** 304 | * Executes the query and returns the first result or throws if the query returned 305 | * no result. 306 | * 307 | * By default an instance of {@link NoResultError} is thrown, but you can provide 308 | * a custom error class as the only argument to throw a different error. 309 | */ 310 | async executeTakeFirstOrThrow(errorConstructor: NoResultErrorConstructor = NoResultError): Promise { 311 | const result = await this.executeTakeFirst() 312 | 313 | if (result === undefined) { 314 | throw new errorConstructor(this.toOperationNode() as any) 315 | } 316 | 317 | return result 318 | } 319 | } 320 | 321 | preventAwait( 322 | RelateQueryBuilder, 323 | "don't await RelateQueryBuilder instances directly. To execute the query you need to call `execute` or `executeTakeFirst`.", 324 | ) 325 | 326 | interface RelateQueryBuilderProps { 327 | executor: QueryExecutor 328 | queryId: QueryId 329 | queryNode: RelateQueryNode 330 | } 331 | -------------------------------------------------------------------------------- /src/query-builder/return-interface.ts: -------------------------------------------------------------------------------- 1 | import type {ReturnExpression} from '../parser/return-parser.js' 2 | 3 | export interface ReturnInterface { 4 | /** 5 | * Allows controlling the returned value for a {@link SurrealKysely} query, using 6 | * the return clause. 7 | * 8 | * SurrealDB returns the created record/s by default. This can be changed by selecting 9 | * specific columns, or using a reserved keyword such as `'none'`, `'diff'`, `'before'` 10 | * & `'after'`. 11 | * 12 | * ### Examples 13 | * 14 | * ```ts 15 | * await db 16 | * .create('person') 17 | * .set({ 18 | * age: 46, 19 | * username: 'john-smith', 20 | * }) 21 | * .return('none') 22 | * .execute() 23 | * ``` 24 | * 25 | * The generated SurrealQL: 26 | * 27 | * ```sql 28 | * let $1 = 46; 29 | * let $2 = 'john-smith'; 30 | * create person set age = $1, username = $2 return none; 31 | * ``` 32 | * 33 | * ... 34 | * 35 | * ```ts 36 | * const person = await db 37 | * .create('person') 38 | * .set({ 39 | * age: 46, 40 | * username: 'john-smith', 41 | * interests: ['skiing', 'music'], 42 | * }) 43 | * .return(['username', 'interests']) 44 | * .executeTakeFirst() 45 | * ``` 46 | * 47 | * The generated SurrealQL: 48 | * 49 | * ```sql 50 | * let $1 = 46; 51 | * let $2 = 'john-smith'; 52 | * let $3 = [\"skiing\",\"music\"]; 53 | * create person 54 | * set age = $1, username = $2, interests = $3 55 | * return username, interests; 56 | * ``` 57 | */ 58 | return>(expression: RE): ReturnInterface 59 | } 60 | -------------------------------------------------------------------------------- /src/query-builder/set-content-interface.ts: -------------------------------------------------------------------------------- 1 | import type {CreateObject} from '../parser/create-object-parser.js' 2 | 3 | export interface SetContentInterface { 4 | /** 5 | * Sets the created relation's values for a {@link SurrealKysely} query, using 6 | * the content clause. 7 | * 8 | * This method takes an object whose keys are column names and values are values 9 | * to insert. In addition to the column's type, the values can be raw {@link sql} 10 | * snippets or select queries. 11 | * 12 | * Nested builders in content objects are not supported yet. You can pass raw 13 | * SurrealQL as a workaround. 14 | * 15 | * You must provide all fields you haven't explicitly marked as nullable or optional 16 | * using {@link Generated} or {@link ColumnType} 17 | * 18 | * This query returns the created relation/s by default. See the {@link return} 19 | * method for a way to control the returned data. 20 | * 21 | * ### Examples 22 | * 23 | * ```ts 24 | * import {sql} from 'kysely' 25 | * 26 | * const relation = await db 27 | * .relate('write') 28 | * .from('user', 'tobie') 29 | * .to('article', 'surreal') 30 | * .content(sql`{source: 'Apple notes', tags: ['notes', 'markdown'], time: {written: time::now()}}`) 31 | * .executeTakeFirst() 32 | * ``` 33 | * 34 | * The generated SurrealQL: 35 | * 36 | * ```sql 37 | * relate user:tobie -> write -> article:surreal 38 | * content {source: 'Apple notes', tags: ['notes', 'markdown'], time: {written: time::now()}}; 39 | * ``` 40 | */ 41 | content>(content: C): SetContentInterface 42 | 43 | /** 44 | * Sets the created relation's values for a {@link SurrealKysely} query, using 45 | * the set clause. 46 | * 47 | * This method takes an object whose keys are column names (or nested object column paths) and values are values 48 | * to insert. In addition to the column's type, the values can be raw {@link sql} 49 | * snippets or select queries. 50 | * 51 | * You must provide all fields you haven't explicitly marked as nullable or optional 52 | * using {@link Generated} or {@link ColumnType} 53 | * 54 | * This query returns the created relation/s by default. See the {@link return} method 55 | * for a way to control the returned data. 56 | * 57 | * ### Examples 58 | * 59 | * ```ts 60 | * import {sql} from 'kysely' 61 | * 62 | * const relation = await db 63 | * .relate('write') 64 | * .from('user:tobie') 65 | * .to('article:surreal') 66 | * .set({ 67 | * 'time.written': sql`time::now()`, 68 | * }) 69 | * .executeTakeFirst() 70 | * ``` 71 | * 72 | * The generated SurrealQL: 73 | * 74 | * ```sql 75 | * relate user:tobie -> write -> article:surreal 76 | * set time.written = time::now(); 77 | * ``` 78 | */ 79 | set>(values: V): SetContentInterface 80 | } 81 | -------------------------------------------------------------------------------- /src/query-compiler/query-compiler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultQueryCompiler, 3 | RawNode, 4 | SelectQueryNode, 5 | type OffsetNode, 6 | type OperationNode, 7 | type RootOperationNode, 8 | } from 'kysely' 9 | 10 | import type {CreateQueryNode} from '../operation-node/create-query-node.js' 11 | import type {ElseIfNode} from '../operation-node/else-if-node.js' 12 | import type {IfElseQueryNode} from '../operation-node/if-else-query-node.js' 13 | import { 14 | isSurrealOperationNode, 15 | type SurrealOperationNode, 16 | type SurrealOperationNodeKind, 17 | } from '../operation-node/operation-node.js' 18 | import type {RelateQueryNode} from '../operation-node/relate-query-node.js' 19 | import type {ReturnNode} from '../operation-node/return-node.js' 20 | import type {VertexNode} from '../operation-node/vertex-node.js' 21 | import {isSurrealReturnType} from '../parser/return-parser.js' 22 | import {freeze, isReadonlyArray} from '../util/object-utils.js' 23 | 24 | export class SurrealDbQueryCompiler extends DefaultQueryCompiler { 25 | protected appendRootOperationNodeAsValue(node: RootOperationNode): void { 26 | const {parameters, sql} = new SurrealDbQueryCompiler().compileQuery(node) 27 | 28 | parameters.forEach((parameter) => this.appendValue(parameter)) 29 | this.appendValue(`SURREALQL::(${sql})`) 30 | } 31 | 32 | protected override getLeftIdentifierWrapper(): string { 33 | return '' 34 | } 35 | 36 | protected override getRightIdentifierWrapper(): string { 37 | return '' 38 | } 39 | 40 | readonly #surrealVisitors: Record = freeze({ 41 | CreateQueryNode: this.visitCreateQuery.bind(this), 42 | ElseIfNode: this.visitElseIf.bind(this), 43 | IfElseQueryNode: this.visitIfElseQuery.bind(this), 44 | RelateQueryNode: this.visitRelateQuery.bind(this), 45 | ReturnNode: this.visitReturn.bind(this), 46 | VertexNode: this.visitVertex.bind(this), 47 | }) 48 | 49 | protected readonly superVisitNode = this.visitNode 50 | protected readonly visitNode = (node: OperationNode): void => { 51 | if (!isSurrealOperationNode(node)) { 52 | return this.superVisitNode(node) 53 | } 54 | 55 | this.nodeStack.push(node) 56 | this.#surrealVisitors[(node as SurrealOperationNode).kind](node) 57 | this.nodeStack.pop() 58 | } 59 | 60 | protected visitCreateQuery(node: CreateQueryNode): void { 61 | this.append('create ') 62 | this.visitNode(node.target) 63 | 64 | if (node.content) { 65 | this.append(' content ') 66 | this.visitNode(node.content) 67 | } 68 | 69 | if (node.set) { 70 | this.append(' set ') 71 | this.compileList(node.set) 72 | } 73 | 74 | if (node.return) { 75 | this.append(' ') 76 | this.visitNode(node.return as any) 77 | } 78 | } 79 | 80 | protected visitElseIf(node: ElseIfNode): void { 81 | this.append('else if ') 82 | this.visitNode(node.if) 83 | 84 | if (node.then) { 85 | this.append(' then ') 86 | this.visitNode(node.then as any) 87 | } 88 | } 89 | 90 | protected visitIfElseQuery(node: IfElseQueryNode): void { 91 | this.append('if ') 92 | this.visitNode(node.if) 93 | 94 | if (node.then) { 95 | this.append(' then ') 96 | this.visitNode(node.then as any) 97 | } 98 | 99 | if (node.elseIf && node.elseIf.length > 0) { 100 | this.append(' ') 101 | this.compileList(node.elseIf as any, ' ') 102 | } 103 | 104 | if (node.else) { 105 | this.append(' else ') 106 | this.visitNode(node.else as any) 107 | } 108 | 109 | this.append(' end') 110 | } 111 | 112 | protected override visitOffset(node: OffsetNode): void { 113 | this.append('start ') 114 | this.visitNode(node.offset) 115 | } 116 | 117 | protected visitRelateQuery(node: RelateQueryNode): void { 118 | const {content, from, set, to} = node 119 | 120 | this.append('relate') 121 | 122 | if (from) { 123 | this.append(' ') 124 | this.visitNode(from as any) 125 | this.append('->') 126 | } 127 | 128 | this.visitNode(node.edge) 129 | 130 | if (to) { 131 | this.append('->') 132 | this.visitNode(to as any) 133 | } 134 | 135 | if (content) { 136 | this.append(' content ') 137 | this.visitNode(content) 138 | } 139 | 140 | if (set) { 141 | this.append(' set ') 142 | this.compileList(set) 143 | } 144 | 145 | if (node.return) { 146 | this.append(' ') 147 | this.visitNode(node.return as any) 148 | } 149 | } 150 | 151 | protected visitReturn(node: ReturnNode): void { 152 | this.append('return ') 153 | 154 | if (isSurrealReturnType(node.return)) { 155 | return this.append(node.return) 156 | } 157 | 158 | if (Array.isArray(node.return)) { 159 | return this.compileList(node.return) 160 | } 161 | 162 | this.visitNode(node.return as any) 163 | } 164 | 165 | protected visitVertex(node: VertexNode): void { 166 | const {vertex} = node 167 | 168 | if (isReadonlyArray(vertex)) { 169 | this.append('[') 170 | this.compileList(vertex) 171 | this.append(']') 172 | } else if (SelectQueryNode.is(vertex) || RawNode.is(vertex)) { 173 | this.appendRootOperationNodeAsValue(vertex) 174 | } else { 175 | this.visitNode(vertex) 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/surreal-kysely.ts: -------------------------------------------------------------------------------- 1 | import {Kysely, TableNode, type Expression} from 'kysely' 2 | 3 | import {CreateQueryNode} from './operation-node/create-query-node.js' 4 | import {IfElseQueryNode} from './operation-node/if-else-query-node.js' 5 | import {RelateQueryNode} from './operation-node/relate-query-node.js' 6 | import {CreateQueryBuilder} from './query-builder/create-query-builder.js' 7 | import {IfElseQueryBuilder} from './query-builder/if-else-query-builder.js' 8 | import {RelateQueryBuilder} from './query-builder/relate-query-builder.js' 9 | import {createQueryId} from './util/query-id.js' 10 | import type {AnyEdge, SurrealDatabase, SurrealRecordId} from './util/surreal-types.js' 11 | 12 | /** 13 | * The main SurrealKysely class. 14 | * 15 | * You should create one instance of `SurrealKysely` per database & namespace 16 | * using the {@link SurrealKysely} constructor. 17 | * 18 | * ### Examples 19 | * 20 | * This example assumes your database has tables `person` and `pet` and there's 21 | * and `own` relation between them. 22 | * 23 | * ```ts 24 | * import type {Generated} from 'kysely' 25 | * import {SurrealDbHttpDialect, SurrealKysely} from 'kysely-surrealdb' 26 | * import {fetch} from 'undici' 27 | * 28 | * interface Person { 29 | * first_name: string 30 | * last_name: string 31 | * } 32 | * 33 | * interface Pet { 34 | * name: string 35 | * species: 'cat' | 'dog' 36 | * } 37 | * 38 | * interface Own { 39 | * time: { 40 | * adopted: string 41 | * } 42 | * } 43 | * 44 | * interface Database { 45 | * person: Person 46 | * own: SurrealEdge 47 | * pet: Pet 48 | * } 49 | * 50 | * const db = new SurrealKysely({ 51 | * dialect: new SurrealDbHttpDialect({ 52 | * database: 'test', 53 | * fetch, 54 | * hostname: 'localhost:8000', 55 | * namespace: 'test', 56 | * password: 'root', 57 | * username: 'root', 58 | * }), 59 | * }) 60 | * ``` 61 | * 62 | * @typeParam DB - The database interface type. Keys of this type must be table names 63 | * in the database and values must be interfaces that describe the rows in those 64 | * tables. See the examples above. 65 | */ 66 | export class SurrealKysely extends Kysely> { 67 | /** 68 | * Creates a create query. 69 | * 70 | * This query returns the created record by default. See the 71 | * {@link CreateQueryBuilder.return | return} method for a way to control the 72 | * returned data. 73 | * 74 | * ### Examples 75 | * 76 | * ```ts 77 | * const tobie = await db 78 | * .create('person') 79 | * .set({ 80 | * name: 'Tobie', 81 | * company: 'SurrealDB', 82 | * skills: ['Rust', 'Go', 'JavaScript'], 83 | * }) 84 | * .executeTakeFirst() 85 | * ``` 86 | * 87 | * The generated SurrealQL: 88 | * 89 | * ```sql 90 | * let $1 = 'Tobie'; 91 | * let $2 = 'SurrealDB'; 92 | * let $3 = [\"Rust\",\"Go\",\"JavaScript\"]; 93 | * create person set name = $1, company = $2, skills = $3; 94 | * ``` 95 | * 96 | * ... 97 | * 98 | * ```ts 99 | * await db 100 | * .create('person:tobie') 101 | * .content({ 102 | * name: 'Tobie', 103 | * company: 'SurrealDB', 104 | * skills: ['Rust', 'Go', 'JavaScript'], 105 | * }) 106 | * .return('none') 107 | * .execute() 108 | * ``` 109 | * 110 | * The generated SurrealQL: 111 | * 112 | * ```sql 113 | * let $1 = {\"name\":\"Tobie\",\"company\":\"SurrealDB\",\"skills\":[\"Rust\",\"Go\",\"JavaScript\"]}; 114 | * create person:tobie content $1 return none; 115 | * ``` 116 | */ 117 | create(table: TB, id?: string | number): CreateQueryBuilder, TB> 118 | 119 | create>(record: R): CreateQueryBuilder, R> 120 | 121 | create>(target: T, id?: string | number): any { 122 | const ref = id !== undefined ? `${String(target)}:${id}` : String(target) 123 | 124 | return new CreateQueryBuilder({ 125 | executor: this.getExecutor(), 126 | queryId: createQueryId(), 127 | queryNode: CreateQueryNode.create(TableNode.create(ref)), 128 | }) 129 | } 130 | 131 | ifThen(condition: Expression, expression: Expression): IfElseQueryBuilder { 132 | return new IfElseQueryBuilder({ 133 | executor: this.getExecutor(), 134 | queryId: createQueryId(), 135 | queryNode: IfElseQueryNode.create(condition.toOperationNode(), expression.toOperationNode()), 136 | }) 137 | } 138 | 139 | /** 140 | * Creates a relate query. 141 | * 142 | * This query returns the created relation by default. See the {@link RelateQueryBuilder.return | return} 143 | * method for a way to control the returned data. 144 | * 145 | * This method only accepts tables that are defined as {@link SurrealEdge}s. 146 | * 147 | * ### Examples 148 | * 149 | * ```ts 150 | * import {sql} from 'kysely' 151 | * 152 | * const relation = await db 153 | * .relate('write') 154 | * .from('user:tobie') 155 | * .to('article:surreal') 156 | * .set({ 157 | * 'time.written': sql`time::now()`, 158 | * }) 159 | * .executeTakeFirst() 160 | * ``` 161 | * 162 | * The generated SurrealQL: 163 | * 164 | * ```sql 165 | * relate user:tobie -> write -> article:surreal 166 | * set time.written = time::now(); 167 | * ``` 168 | * 169 | * ... 170 | * 171 | * ```ts 172 | * import {sql} from 'kysely' 173 | * 174 | * await db 175 | * .relate('like') 176 | * .from(db.selectFrom('company:surrealdb').select('users')) 177 | * .to( 178 | * db 179 | * .selectFrom('user') 180 | * .where(sql`${sql.ref('tags')} contains 'developer'`) 181 | * .selectAll(), 182 | * ) 183 | * .set({ 184 | * 'time.connected': sql`time::now()`, 185 | * }) 186 | * .execute() 187 | * ``` 188 | * 189 | * The generated SurrealQL: 190 | * 191 | * ```sql 192 | * let $1 = (select users from company:surrealdb); 193 | * let $2 = (select * from user where tags contains 'developer'); 194 | * relate $1 -> like -> $2 set time.connected = time::now(); 195 | * ``` 196 | */ 197 | relate>(edge: E): RelateQueryBuilder, E> { 198 | return new RelateQueryBuilder({ 199 | executor: this.getExecutor(), 200 | queryId: createQueryId(), 201 | queryNode: RelateQueryNode.create(TableNode.create(edge)), 202 | }) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/util/encode-to-base64.ts: -------------------------------------------------------------------------------- 1 | export function encodeToBase64(str: string): string { 2 | return typeof process === 'undefined' ? btoa(str) : Buffer.from(str).toString('base64') 3 | } 4 | -------------------------------------------------------------------------------- /src/util/object-utils.ts: -------------------------------------------------------------------------------- 1 | export function freeze(obj: T): Readonly { 2 | return Object.freeze(obj) 3 | } 4 | 5 | export function isReadonlyArray(obj: unknown): obj is ReadonlyArray { 6 | return Array.isArray(obj) 7 | } 8 | -------------------------------------------------------------------------------- /src/util/prevent-await.ts: -------------------------------------------------------------------------------- 1 | export function preventAwait(clazz: Function, message: string): void { 2 | Object.defineProperties(clazz.prototype, { 3 | then: { 4 | enumerable: false, 5 | value: () => { 6 | throw new Error(message) 7 | }, 8 | }, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/util/query-id.ts: -------------------------------------------------------------------------------- 1 | import {randomString} from './random-string.js' 2 | 3 | export interface QueryId { 4 | readonly queryId: string 5 | } 6 | 7 | export function createQueryId(): QueryId { 8 | return new LazyQueryId() 9 | } 10 | 11 | class LazyQueryId implements QueryId { 12 | #queryId: string | undefined 13 | 14 | get queryId(): string { 15 | if (this.#queryId === undefined) { 16 | this.#queryId = randomString(8) 17 | } 18 | 19 | return this.#queryId 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/util/random-string.ts: -------------------------------------------------------------------------------- 1 | const CHARS = [ 2 | 'A', 3 | 'B', 4 | 'C', 5 | 'D', 6 | 'E', 7 | 'F', 8 | 'G', 9 | 'H', 10 | 'I', 11 | 'J', 12 | 'K', 13 | 'L', 14 | 'M', 15 | 'N', 16 | 'O', 17 | 'P', 18 | 'Q', 19 | 'R', 20 | 'S', 21 | 'T', 22 | 'U', 23 | 'V', 24 | 'W', 25 | 'X', 26 | 'Y', 27 | 'Z', 28 | 'a', 29 | 'b', 30 | 'c', 31 | 'd', 32 | 'e', 33 | 'f', 34 | 'g', 35 | 'h', 36 | 'i', 37 | 'j', 38 | 'k', 39 | 'l', 40 | 'm', 41 | 'n', 42 | 'o', 43 | 'p', 44 | 'q', 45 | 'r', 46 | 's', 47 | 't', 48 | 'u', 49 | 'v', 50 | 'w', 51 | 'x', 52 | 'y', 53 | 'z', 54 | '0', 55 | '1', 56 | '2', 57 | '3', 58 | '4', 59 | '5', 60 | '6', 61 | '7', 62 | '8', 63 | '9', 64 | ] 65 | 66 | export function randomString(length: number): string { 67 | let chars = '' 68 | 69 | for (let i = 0; i < length; ++i) { 70 | chars += randomChar() 71 | } 72 | 73 | return chars 74 | } 75 | 76 | function randomChar() { 77 | return CHARS[~~(Math.random() * CHARS.length)] 78 | } 79 | -------------------------------------------------------------------------------- /src/util/surreal-types.ts: -------------------------------------------------------------------------------- 1 | import type {ColumnType, GeneratedAlways} from 'kysely' 2 | 3 | /** 4 | * Enhances a regular database interface with SurrealDB stuff. 5 | * 6 | * Automatically adds id columns, and edges' (see {@link SurrealEdge}) in-out columns 7 | * so you don't have to. 8 | * 9 | * When using {@link SurrealKysely}, you only need to pass your regular database 10 | * interface - the wrapper does the wrapping for you under the hood. 11 | * 12 | * ### Examples 13 | * 14 | * ```ts 15 | * interface Person { 16 | * first_name: string 17 | * last_name: string 18 | * } 19 | * 20 | * interface Pet { 21 | * name: string 22 | * species: 'cat' | 'dog' 23 | * } 24 | * 25 | * interface Own { 26 | * time: { 27 | * adopted: string 28 | * } 29 | * } 30 | * 31 | * interface Database { 32 | * person: Person 33 | * own: SurrealEdge 34 | * pet: Pet 35 | * } 36 | * 37 | * SurrealDatabase 38 | * ``` 39 | * 40 | * @typeParam DB - The database interface type. Keys of this type must be table names 41 | * in the database and values must be interfaces that describe the rows in those 42 | * tables. See the examples above. 43 | */ 44 | export type SurrealDatabase = { 45 | [K in keyof DB | SurrealRecordId]: K extends `${infer TB}:${string}` 46 | ? TB extends keyof DB 47 | ? SurrealRecordOrEdge 48 | : never 49 | : K extends keyof DB 50 | ? SurrealRecordOrEdge 51 | : never 52 | } 53 | 54 | export type SurrealRecordId = TB extends string ? `${TB}:${string}` : never 55 | 56 | export type SurrealRecordOrEdge = DB[TB] extends SurrealEdge 57 | ? { 58 | [C in 'id' | 'in' | 'out' | keyof DB[TB]]: C extends 'id' 59 | ? ColumnType, string | undefined, string | undefined> 60 | : C extends 'in' | 'out' 61 | ? GeneratedAlways> 62 | : C extends keyof DB[TB] 63 | ? DB[TB][C] 64 | : never 65 | } 66 | : SurrealRecord 67 | 68 | export type SurrealRecord = { 69 | [C in 'id' | keyof DB[TB]]: C extends 'id' 70 | ? ColumnType, string | undefined, string | undefined> 71 | : C extends keyof DB[TB] 72 | ? DB[TB][C] 73 | : never 74 | } 75 | 76 | /** 77 | * Gives a hint to {@link SurrealDatabase} that the given table is a graph edge. 78 | * 79 | * These tables can act as graph edges between two records in {@link SurrealKysely.relate | relate} 80 | * queries. 81 | * 82 | * ### Examples 83 | * 84 | * ```ts 85 | * interface Database { 86 | * person: Person 87 | * own: SurrealEdge 88 | * pet: Pet 89 | * } 90 | * ``` 91 | * 92 | * @typeParam E - An interface representing the edge's unreserved columns structure. 93 | */ 94 | export type SurrealEdge = { 95 | [K in '__edge' | keyof E]: K extends '__edge' ? ColumnType : K extends keyof E ? E[K] : never 96 | } 97 | 98 | export type AnyEdge = { 99 | [K in keyof DB]: DB[K] extends SurrealEdge ? (K extends string ? K : never) : never 100 | }[keyof DB] 101 | 102 | export type AnySpecificVertex = { 103 | [K in keyof DB]: DB[K] extends SurrealEdge ? never : SurrealRecordId 104 | }[keyof DB] 105 | 106 | export type AnyVertexGroup = { 107 | [K in keyof DB]: K extends AnyEdge 108 | ? never 109 | : K extends `${string}:${string}` 110 | ? never 111 | : K extends string 112 | ? K 113 | : never 114 | }[keyof DB] 115 | -------------------------------------------------------------------------------- /src/util/type-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes all properties from T1 and merges all properties from T2 3 | * that don't exist in T1 as optional properties. 4 | * 5 | * Example: 6 | * 7 | * interface Person { 8 | * name: string 9 | * age: number 10 | * } 11 | * 12 | * interface Pet { 13 | * name: string 14 | * species: 'cat' | 'dog' 15 | * } 16 | * 17 | * type Merged = MergePartial 18 | * 19 | * // { name: string, age: number, species?: 'cat' | 'dog' } 20 | */ 21 | export type MergePartial = T1 & Partial> 22 | -------------------------------------------------------------------------------- /tests/nodejs/http-dialect.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {Kysely, type ColumnType, type GeneratedAlways} from 'kysely' 3 | import nodeFetch from 'node-fetch' 4 | import {fetch as undiciFetch} from 'undici' 5 | 6 | import {SurrealDbHttpDialect, type SurrealDbHttpDialectConfig} from '../../src' 7 | 8 | interface Database { 9 | person: Person 10 | pet: Pet 11 | toy: Toy 12 | } 13 | 14 | interface Person { 15 | id: GeneratedAlways 16 | first_name: string | null 17 | middle_name: string | null 18 | last_name: string | null 19 | age: number 20 | gender: 'male' | 'female' | 'other' 21 | } 22 | 23 | interface Pet { 24 | id: GeneratedAlways 25 | name: string 26 | owner_id: number 27 | species: 'cat' | 'dog' | 'hamster' 28 | } 29 | 30 | interface Toy { 31 | id: ColumnType 32 | name: string 33 | price: ColumnType 34 | pet_id: number 35 | } 36 | 37 | describe('SurrealDbHttpDialect', () => { 38 | let db: Kysely 39 | 40 | before(async () => { 41 | db = getDB() 42 | }) 43 | 44 | it('should execute a query with parameters.', async () => { 45 | const actual = await db 46 | .selectFrom('person') 47 | .selectAll() 48 | .where('age', '>=', 15) 49 | .where('first_name', '=', 'Jennifer') 50 | .execute() 51 | 52 | expect(actual).to.be.an('array') 53 | }) 54 | }) 55 | 56 | function getDB(config?: Partial): Kysely { 57 | return new Kysely({ 58 | dialect: new SurrealDbHttpDialect({ 59 | database: 'test', 60 | fetch: getFetch(), 61 | hostname: 'localhost:8000', 62 | namespace: 'test', 63 | password: 'root', 64 | username: 'root', 65 | ...config, 66 | }), 67 | }) 68 | } 69 | 70 | function getFetch() { 71 | const {version} = process 72 | 73 | if (version.startsWith('v18')) { 74 | return fetch 75 | } 76 | 77 | if (version.startsWith('v16')) { 78 | return undiciFetch 79 | } 80 | 81 | return nodeFetch 82 | } 83 | -------------------------------------------------------------------------------- /tests/nodejs/surreal-kysely/basic-types.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | 3 | import {FALSE, NONE, NULL, TRUE} from '../../../src/helpers' 4 | import {DIALECTS, dropTables, initTests, testSurrealQl, type TestContext} from './shared' 5 | 6 | DIALECTS.forEach((dialect) => { 7 | describe(`${dialect}: Basic types`, () => { 8 | let ctx: TestContext 9 | 10 | before(async () => { 11 | ctx = initTests(dialect) 12 | }) 13 | 14 | afterEach(async () => { 15 | await dropTables(ctx, ['person']) 16 | }) 17 | 18 | after(async () => { 19 | await ctx.db.destroy() 20 | }) 21 | 22 | it('should support none values.', async () => { 23 | const query = ctx.db.create('person').set({children: NONE}) 24 | 25 | testSurrealQl(query, { 26 | sql: 'create person set children = none', 27 | parameters: [], 28 | }) 29 | 30 | const actual = await query.execute() 31 | 32 | expect(actual).to.be.an('array').which.has.lengthOf(1) 33 | }) 34 | 35 | it('should support null values.', async () => { 36 | const query = ctx.db.create('person').set({children: NULL}) 37 | 38 | testSurrealQl(query, { 39 | sql: 'create person set children = null', 40 | parameters: [], 41 | }) 42 | 43 | const actual = await query.execute() 44 | 45 | expect(actual).to.be.an('array').which.has.lengthOf(1) 46 | }) 47 | 48 | it('should support booleans.', async () => { 49 | const query = ctx.db.create('person').set({newsletter: FALSE, interested: TRUE}) 50 | 51 | testSurrealQl(query, { 52 | sql: 'create person set newsletter = false, interested = true', 53 | parameters: [], 54 | }) 55 | 56 | const actual = await query.execute() 57 | 58 | expect(actual).to.be.an('array').which.has.lengthOf(1) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/nodejs/surreal-kysely/create.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | 3 | import {DIALECTS, dropTables, initTests, prepareTables, testSurrealQl, type TestContext} from './shared' 4 | 5 | DIALECTS.forEach((dialect) => { 6 | describe(`${dialect}: SurrealKysely.create(...)`, () => { 7 | let ctx: TestContext 8 | 9 | before(async () => { 10 | ctx = initTests(dialect) 11 | }) 12 | 13 | beforeEach(async () => { 14 | await prepareTables(ctx, ['person']) 15 | }) 16 | 17 | afterEach(async () => { 18 | await dropTables(ctx, ['person']) 19 | }) 20 | 21 | after(async () => { 22 | await ctx.db.destroy() 23 | }) 24 | 25 | it('should execute a create...set query with a random id.', async () => { 26 | const query = ctx.db.create('person').set({ 27 | name: 'Tobie', 28 | company: 'SurrealDB', 29 | skills: ['Rust', 'Go', 'JavaScript'], 30 | }) 31 | 32 | testSurrealQl(query, { 33 | sql: 'create person set name = $1, company = $2, skills = $3', 34 | parameters: ['Tobie', 'SurrealDB', ['Rust', 'Go', 'JavaScript']], 35 | }) 36 | 37 | const actual = await query.execute() 38 | 39 | expect(actual).to.be.an('array').which.has.lengthOf(1) 40 | }) 41 | 42 | it('should execute a create...set query with a specific numeric id.', async () => { 43 | const query = ctx.db.create('person:100').set({ 44 | name: 'Tobie', 45 | company: 'SurrealDB', 46 | skills: ['Rust', 'Go', 'JavaScript'], 47 | }) 48 | 49 | testSurrealQl(query, { 50 | sql: 'create person:100 set name = $1, company = $2, skills = $3', 51 | parameters: ['Tobie', 'SurrealDB', ['Rust', 'Go', 'JavaScript']], 52 | }) 53 | 54 | const actual = await query.execute() 55 | 56 | expect(actual).to.be.an('array').which.has.lengthOf(1) 57 | expect(actual[0]).to.deep.equal({ 58 | id: 'person:100', 59 | name: 'Tobie', 60 | company: 'SurrealDB', 61 | skills: ['Rust', 'Go', 'JavaScript'], 62 | }) 63 | }) 64 | 65 | it('should execute a create...set query with a specific string id.', async () => { 66 | const query = ctx.db.create('person:tobie').set({ 67 | name: 'Tobie', 68 | company: 'SurrealDB', 69 | skills: ['Rust', 'Go', 'JavaScript'], 70 | }) 71 | 72 | testSurrealQl(query, { 73 | sql: 'create person:tobie set name = $1, company = $2, skills = $3', 74 | parameters: ['Tobie', 'SurrealDB', ['Rust', 'Go', 'JavaScript']], 75 | }) 76 | 77 | const actual = await query.execute() 78 | 79 | expect(actual).to.be.an('array').which.has.lengthOf(1) 80 | expect(actual[0]).to.deep.equal({ 81 | id: 'person:tobie', 82 | name: 'Tobie', 83 | company: 'SurrealDB', 84 | skills: ['Rust', 'Go', 'JavaScript'], 85 | }) 86 | }) 87 | 88 | it('should execute a create...content query with a random id.', async () => { 89 | const query = ctx.db.create('person').content({ 90 | name: 'Tobie', 91 | company: 'SurrealDB', 92 | skills: ['Rust', 'Go', 'JavaScript'], 93 | }) 94 | 95 | testSurrealQl(query, { 96 | sql: 'create person content $1', 97 | parameters: [ 98 | { 99 | name: 'Tobie', 100 | company: 'SurrealDB', 101 | skills: ['Rust', 'Go', 'JavaScript'], 102 | }, 103 | ], 104 | }) 105 | 106 | const actual = await query.execute() 107 | 108 | expect(actual).to.be.an('array').which.has.lengthOf(1) 109 | }) 110 | 111 | it('should execute a create...content query with a specific id.', async () => { 112 | const query = ctx.db.create('person:tobie2').content({ 113 | name: 'Tobie', 114 | company: 'SurrealDB', 115 | skills: ['Rust', 'Go', 'JavaScript'], 116 | }) 117 | 118 | testSurrealQl(query, { 119 | sql: 'create person:tobie2 content $1', 120 | parameters: [ 121 | { 122 | name: 'Tobie', 123 | company: 'SurrealDB', 124 | skills: ['Rust', 'Go', 'JavaScript'], 125 | }, 126 | ], 127 | }) 128 | 129 | const actual = await query.execute() 130 | 131 | expect(actual).to.be.an('array').which.has.lengthOf(1) 132 | expect(actual[0]).to.deep.equal({ 133 | id: 'person:tobie2', 134 | name: 'Tobie', 135 | company: 'SurrealDB', 136 | skills: ['Rust', 'Go', 'JavaScript'], 137 | }) 138 | }) 139 | 140 | it('should execute a create...set...return none query.', async () => { 141 | const query = ctx.db 142 | .create('person') 143 | .set({ 144 | age: 46, 145 | username: 'john-smith', 146 | }) 147 | .return('none') 148 | 149 | testSurrealQl(query, { 150 | sql: 'create person set age = $1, username = $2 return none', 151 | parameters: [46, 'john-smith'], 152 | }) 153 | 154 | const actual = await query.execute() 155 | 156 | expect(actual).to.be.an('array').that.is.empty 157 | }) 158 | 159 | it('should execute a create...set...return diff query.', async () => { 160 | const query = ctx.db 161 | .create('person') 162 | .set({ 163 | age: 46, 164 | username: 'john-smith', 165 | }) 166 | .return('diff') 167 | 168 | testSurrealQl(query, { 169 | sql: 'create person set age = $1, username = $2 return diff', 170 | parameters: [46, 'john-smith'], 171 | }) 172 | 173 | const actual = await query.execute() 174 | 175 | expect(actual).to.be.an('array').which.has.lengthOf(1) 176 | }) 177 | 178 | it('should execute a create...set...return before query.', async () => { 179 | const query = ctx.db 180 | .create('person') 181 | .set({ 182 | age: 46, 183 | username: 'john-smith', 184 | }) 185 | .return('before') 186 | 187 | testSurrealQl(query, { 188 | sql: 'create person set age = $1, username = $2 return before', 189 | parameters: [46, 'john-smith'], 190 | }) 191 | 192 | const actual = await query.execute() 193 | 194 | expect(actual).to.be.an('array').which.has.lengthOf(1) 195 | }) 196 | 197 | it('should execute a create...set...return after query.', async () => { 198 | const query = ctx.db 199 | .create('person') 200 | .set({ 201 | age: 46, 202 | username: 'john-smith', 203 | }) 204 | .return('after') 205 | 206 | testSurrealQl(query, { 207 | sql: 'create person set age = $1, username = $2 return after', 208 | parameters: [46, 'john-smith'], 209 | }) 210 | 211 | const actual = await query.execute() 212 | 213 | expect(actual).to.be.an('array').which.has.lengthOf(1) 214 | }) 215 | 216 | it('should execute a create...set...return field query.', async () => { 217 | const query = ctx.db 218 | .create('person') 219 | .set({ 220 | age: 46, 221 | username: 'john-smith', 222 | interests: ['skiing', 'music'], 223 | }) 224 | .return('interests') 225 | 226 | testSurrealQl(query, { 227 | sql: 'create person set age = $1, username = $2, interests = $3 return interests', 228 | parameters: [46, 'john-smith', ['skiing', 'music']], 229 | }) 230 | 231 | const actual = await query.execute() 232 | 233 | expect(actual).to.be.an('array').which.has.lengthOf(1) 234 | expect(actual[0]).to.deep.equal({interests: ['skiing', 'music']}) 235 | }) 236 | 237 | it('should execute a create...set...return multiple fields query.', async () => { 238 | const query = ctx.db 239 | .create('person') 240 | .set({ 241 | age: 46, 242 | username: 'john-smith', 243 | interests: ['skiing', 'music'], 244 | }) 245 | .return(['name', 'interests']) 246 | 247 | testSurrealQl(query, { 248 | sql: 'create person set age = $1, username = $2, interests = $3 return name, interests', 249 | parameters: [46, 'john-smith', ['skiing', 'music']], 250 | }) 251 | 252 | const actual = await query.execute() 253 | 254 | expect(actual).to.be.an('array').which.has.lengthOf(1) 255 | expect(actual[0]).to.deep.equal({interests: ['skiing', 'music'], name: null}) 256 | }) 257 | 258 | it('should execute a create...set query with table and id as 2 separate arguments.', async () => { 259 | const query = ctx.db.create('person', 'recordid').set({ 260 | age: 46, 261 | username: 'john-smith', 262 | interests: ['skiing', 'music'], 263 | }) 264 | 265 | testSurrealQl(query, { 266 | sql: 'create person:recordid set age = $1, username = $2, interests = $3', 267 | parameters: [46, 'john-smith', ['skiing', 'music']], 268 | }) 269 | 270 | const actual = await query.execute() 271 | 272 | expect(actual).to.be.an('array').which.has.lengthOf(1) 273 | }) 274 | 275 | it('should execute a create...set query with a random id.', async () => { 276 | const query = ctx.db.create('person').set({ 277 | name: 'Tobie', 278 | company: 'SurrealDB', 279 | skills: ['Rust', 'Go', 'JavaScript'], 280 | }) 281 | 282 | testSurrealQl(query, { 283 | sql: 'create person set name = $1, company = $2, skills = $3', 284 | parameters: ['Tobie', 'SurrealDB', ['Rust', 'Go', 'JavaScript']], 285 | }) 286 | 287 | const actual = await query.execute() 288 | 289 | expect(actual).to.be.an('array').which.has.lengthOf(1) 290 | }) 291 | 292 | it('should execute a create...set query with a specific numeric id.', async () => { 293 | const query = ctx.db.create('person:100').set({ 294 | name: 'Tobie', 295 | company: 'SurrealDB', 296 | skills: ['Rust', 'Go', 'JavaScript'], 297 | }) 298 | 299 | testSurrealQl(query, { 300 | sql: 'create person:100 set name = $1, company = $2, skills = $3', 301 | parameters: ['Tobie', 'SurrealDB', ['Rust', 'Go', 'JavaScript']], 302 | }) 303 | 304 | const actual = await query.execute() 305 | 306 | expect(actual).to.be.an('array').which.has.lengthOf(1) 307 | expect(actual[0]).to.deep.equal({ 308 | id: 'person:100', 309 | name: 'Tobie', 310 | company: 'SurrealDB', 311 | skills: ['Rust', 'Go', 'JavaScript'], 312 | }) 313 | }) 314 | 315 | it('should execute a create...set query with a specific string id.', async () => { 316 | const query = ctx.db.create('person:tobie').set({ 317 | name: 'Tobie', 318 | company: 'SurrealDB', 319 | skills: ['Rust', 'Go', 'JavaScript'], 320 | }) 321 | 322 | testSurrealQl(query, { 323 | sql: 'create person:tobie set name = $1, company = $2, skills = $3', 324 | parameters: ['Tobie', 'SurrealDB', ['Rust', 'Go', 'JavaScript']], 325 | }) 326 | 327 | const actual = await query.execute() 328 | 329 | expect(actual).to.be.an('array').which.has.lengthOf(1) 330 | expect(actual[0]).to.deep.equal({ 331 | id: 'person:tobie', 332 | name: 'Tobie', 333 | company: 'SurrealDB', 334 | skills: ['Rust', 'Go', 'JavaScript'], 335 | }) 336 | }) 337 | 338 | it('should execute a create...content query with a random id.', async () => { 339 | const query = ctx.db.create('person').content({ 340 | name: 'Tobie', 341 | company: 'SurrealDB', 342 | skills: ['Rust', 'Go', 'JavaScript'], 343 | }) 344 | 345 | testSurrealQl(query, { 346 | sql: 'create person content $1', 347 | parameters: [ 348 | { 349 | name: 'Tobie', 350 | company: 'SurrealDB', 351 | skills: ['Rust', 'Go', 'JavaScript'], 352 | }, 353 | ], 354 | }) 355 | 356 | const actual = await query.execute() 357 | 358 | expect(actual).to.be.an('array').which.has.lengthOf(1) 359 | }) 360 | 361 | it('should execute a create...content query with a specific id.', async () => { 362 | const query = ctx.db.create('person:tobie2').content({ 363 | name: 'Tobie', 364 | company: 'SurrealDB', 365 | skills: ['Rust', 'Go', 'JavaScript'], 366 | }) 367 | 368 | testSurrealQl(query, { 369 | sql: 'create person:tobie2 content $1', 370 | parameters: [ 371 | { 372 | name: 'Tobie', 373 | company: 'SurrealDB', 374 | skills: ['Rust', 'Go', 'JavaScript'], 375 | }, 376 | ], 377 | }) 378 | 379 | const actual = await query.execute() 380 | 381 | expect(actual).to.be.an('array').which.has.lengthOf(1) 382 | expect(actual[0]).to.deep.equal({ 383 | id: 'person:tobie2', 384 | name: 'Tobie', 385 | company: 'SurrealDB', 386 | skills: ['Rust', 'Go', 'JavaScript'], 387 | }) 388 | }) 389 | 390 | it('should execute a create...set...return none query.', async () => { 391 | const query = ctx.db 392 | .create('person') 393 | .set({ 394 | age: 46, 395 | username: 'john-smith', 396 | }) 397 | .return('none') 398 | 399 | testSurrealQl(query, { 400 | sql: 'create person set age = $1, username = $2 return none', 401 | parameters: [46, 'john-smith'], 402 | }) 403 | 404 | const actual = await query.execute() 405 | 406 | expect(actual).to.be.an('array').that.is.empty 407 | }) 408 | 409 | it('should execute a create...set...return diff query.', async () => { 410 | const query = ctx.db 411 | .create('person') 412 | .set({ 413 | age: 46, 414 | username: 'john-smith', 415 | }) 416 | .return('diff') 417 | 418 | testSurrealQl(query, { 419 | sql: 'create person set age = $1, username = $2 return diff', 420 | parameters: [46, 'john-smith'], 421 | }) 422 | 423 | const actual = await query.execute() 424 | 425 | expect(actual).to.be.an('array').which.has.lengthOf(1) 426 | }) 427 | 428 | it('should execute a create...set...return before query.', async () => { 429 | const query = ctx.db 430 | .create('person') 431 | .set({ 432 | age: 46, 433 | username: 'john-smith', 434 | }) 435 | .return('before') 436 | 437 | testSurrealQl(query, { 438 | sql: 'create person set age = $1, username = $2 return before', 439 | parameters: [46, 'john-smith'], 440 | }) 441 | 442 | const actual = await query.execute() 443 | 444 | expect(actual).to.be.an('array').which.has.lengthOf(1) 445 | }) 446 | 447 | it('should execute a create...set...return after query.', async () => { 448 | const query = ctx.db 449 | .create('person') 450 | .set({ 451 | age: 46, 452 | username: 'john-smith', 453 | }) 454 | .return('after') 455 | 456 | testSurrealQl(query, { 457 | sql: 'create person set age = $1, username = $2 return after', 458 | parameters: [46, 'john-smith'], 459 | }) 460 | 461 | const actual = await query.execute() 462 | 463 | expect(actual).to.be.an('array').which.has.lengthOf(1) 464 | }) 465 | 466 | it('should execute a create...set...return field query.', async () => { 467 | const query = ctx.db 468 | .create('person') 469 | .set({ 470 | age: 46, 471 | username: 'john-smith', 472 | interests: ['skiing', 'music'], 473 | }) 474 | .return('interests') 475 | 476 | testSurrealQl(query, { 477 | sql: 'create person set age = $1, username = $2, interests = $3 return interests', 478 | parameters: [46, 'john-smith', ['skiing', 'music']], 479 | }) 480 | 481 | const actual = await query.execute() 482 | 483 | expect(actual).to.be.an('array').which.has.lengthOf(1) 484 | expect(actual[0]).to.deep.equal({interests: ['skiing', 'music']}) 485 | }) 486 | 487 | it('should execute a create...set...return multiple fields query.', async () => { 488 | const query = ctx.db 489 | .create('person') 490 | .set({ 491 | age: 46, 492 | username: 'john-smith', 493 | interests: ['skiing', 'music'], 494 | }) 495 | .return(['name', 'interests']) 496 | 497 | testSurrealQl(query, { 498 | sql: 'create person set age = $1, username = $2, interests = $3 return name, interests', 499 | parameters: [46, 'john-smith', ['skiing', 'music']], 500 | }) 501 | 502 | const actual = await query.execute() 503 | 504 | expect(actual).to.be.an('array').which.has.lengthOf(1) 505 | expect(actual[0]).to.deep.equal({interests: ['skiing', 'music'], name: null}) 506 | }) 507 | 508 | it('should execute a create...set query with table and id as 2 separate arguments.', async () => { 509 | const query = ctx.db.create('person', 'recordid').set({ 510 | age: 46, 511 | username: 'john-smith', 512 | interests: ['skiing', 'music'], 513 | }) 514 | 515 | testSurrealQl(query, { 516 | sql: 'create person:recordid set age = $1, username = $2, interests = $3', 517 | parameters: [46, 'john-smith', ['skiing', 'music']], 518 | }) 519 | 520 | const actual = await query.execute() 521 | 522 | expect(actual).to.be.an('array').which.has.lengthOf(1) 523 | }) 524 | }) 525 | }) 526 | -------------------------------------------------------------------------------- /tests/nodejs/surreal-kysely/if-else.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {sql, UpdateResult} from 'kysely' 3 | 4 | import {DIALECTS, dropTables, initTests, prepareTables, testSurrealQl, type Account, type TestContext} from './shared' 5 | 6 | DIALECTS.forEach((dialect) => { 7 | describe(`${dialect}: SurrealKysely.ifThen(...)`, () => { 8 | let ctx: TestContext 9 | 10 | before(async () => { 11 | ctx = initTests(dialect) 12 | }) 13 | 14 | beforeEach(async () => { 15 | await prepareTables(ctx, ['account', 'person']) 16 | }) 17 | 18 | afterEach(async () => { 19 | await dropTables(ctx, ['account', 'person']) 20 | }) 21 | 22 | after(async () => { 23 | await ctx.db.destroy() 24 | }) 25 | 26 | it('should execute an if...then...elseif...then...else...end query.', async () => { 27 | const auth = {account: 'account:123'} 28 | 29 | for (const {scope, resultCount} of [ 30 | {scope: 'admin', resultCount: 2}, 31 | {scope: 'user', resultCount: 1}, 32 | {scope: 'moderator', resultCount: 0}, 33 | ]) { 34 | const query = ctx.db 35 | .ifThen(sql`${scope} = ${sql.literal('admin')}`, ctx.db.selectFrom('account').selectAll()) 36 | .elseIfThen(sql`${scope} = ${sql.literal('user')}`, sql`(select * from ${auth}.account)`) 37 | .else(sql<[]>`[]`) 38 | .end() 39 | 40 | testSurrealQl(query, { 41 | sql: [ 42 | `if $1 = 'admin' then (select * from account)`, 43 | `else if $2 = 'user' then (select * from $3.account)`, 44 | 'else [] end', 45 | ].join(' '), 46 | parameters: [scope, scope, auth], 47 | }) 48 | 49 | const actual = await query.execute() 50 | 51 | expect(actual).to.be.an('array').which.has.lengthOf(resultCount) 52 | } 53 | }) 54 | 55 | it('should execute an update...set...if...then...elseif...else...end query.', async () => { 56 | const query = ctx.db.updateTable('person').set({ 57 | railcard: ctx.db 58 | .ifThen(sql`${sql.ref('age')} <= 10`, sql`${sql.literal('junior')}`.$castTo<'junior'>()) 59 | .elseIfThen(sql`${sql.ref('age')} <= 21`, sql.literal('student').$castTo<'student'>()) 60 | .elseIfThen(sql`${sql.ref('age')} >= 65`, sql.literal('senior').$castTo<'senior'>()) 61 | .else(sql`null`) 62 | .end(), 63 | }) 64 | 65 | testSurrealQl(query, { 66 | sql: [ 67 | 'update person', 68 | 'set railcard =', 69 | `if age <= 10 then 'junior'`, 70 | `else if age <= 21 then 'student'`, 71 | `else if age >= 65 then 'senior'`, 72 | 'else null end', 73 | ].join(' '), 74 | parameters: [], 75 | }) 76 | 77 | const actual = await query.executeTakeFirstOrThrow() 78 | 79 | expect(actual).to.be.instanceOf(UpdateResult) 80 | expect(actual.numUpdatedRows).to.be.equal(4n) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /tests/nodejs/surreal-kysely/record-ids.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {sql} from 'kysely' 3 | 4 | import {DIALECTS, dropTables, initTests, testSurrealQl, type TestContext} from './shared' 5 | 6 | DIALECTS.forEach((dialect) => { 7 | describe(`${dialect}: Record IDs`, () => { 8 | let ctx: TestContext 9 | 10 | before(async () => { 11 | ctx = initTests(dialect) 12 | }) 13 | 14 | afterEach(async () => { 15 | await dropTables(ctx, ['article', 'company', 'temperature']) 16 | }) 17 | 18 | after(async () => { 19 | await ctx.db.destroy() 20 | }) 21 | 22 | it('should support simple text record IDs.', async () => { 23 | const query = ctx.db.create('company:surrealdb').set({name: 'SurrealDB'}) 24 | 25 | testSurrealQl(query, { 26 | sql: 'create company:surrealdb set name = $1', 27 | parameters: ['SurrealDB'], 28 | }) 29 | 30 | const actual = await query.execute() 31 | 32 | expect(actual).to.be.an('array').which.has.lengthOf(1) 33 | }) 34 | 35 | it('should support text record IDs with complex characters surrounded by the ` character.', async () => { 36 | const query = ctx.db 37 | .create('article:`8424486b-85b3-4448-ac8d-5d51083391c7`') 38 | .set({time: sql`time::now()`, author: sql`person:tobie`}) 39 | 40 | testSurrealQl(query, { 41 | sql: 'create article:`8424486b-85b3-4448-ac8d-5d51083391c7` set time = time::now(), author = person:tobie', 42 | parameters: [], 43 | }) 44 | 45 | const actual = await query.execute() 46 | 47 | expect(actual).to.be.an('array').which.has.lengthOf(1) 48 | }) 49 | 50 | it('should support text record IDs with complex characters surrounded by the ⟨ and ⟩ characters.', async () => { 51 | const query = ctx.db 52 | .create('article:⟨8424486b-85b3-4448-ac8d-5d51083391c7⟩') 53 | .set({time: sql`time::now()`, author: sql`person:tobie`}) 54 | 55 | testSurrealQl(query, { 56 | sql: 'create article:⟨8424486b-85b3-4448-ac8d-5d51083391c7⟩ set time = time::now(), author = person:tobie', 57 | parameters: [], 58 | }) 59 | 60 | const actual = await query.execute() 61 | 62 | expect(actual).to.be.an('array').which.has.lengthOf(1) 63 | }) 64 | 65 | it('should support numeric record IDs.', async () => { 66 | const query = ctx.db.create('temperature:17493').set({ 67 | time: sql`time::now()`, 68 | celsius: 37.5, 69 | }) 70 | 71 | testSurrealQl(query, { 72 | sql: 'create temperature:17493 set time = time::now(), celsius = $1', 73 | parameters: [37.5], 74 | }) 75 | 76 | const actual = await query.execute() 77 | 78 | expect(actual).to.be.an('array').which.has.lengthOf(1) 79 | }) 80 | 81 | // FIXME: not 1:1 with docs @ https://surrealdb.com/docs/surrealql/datamodel/ids 82 | // no way of using $now in create method yet. 83 | it('should support object-based record IDs.', async () => { 84 | const query = ctx.db.create("temperature:{ location: 'London', date: time::now() }").set({ 85 | location: 'London', 86 | date: sql`string::slice(id, 21, 31)`, 87 | temperature: 23.7, 88 | }) 89 | 90 | testSurrealQl(query, { 91 | sql: "create temperature:{ location: 'London', date: time::now() } set location = $1, date = string::slice(id, 21, 31), temperature = $2", 92 | parameters: ['London', 23.7], 93 | }) 94 | 95 | const actual = await query.execute() 96 | 97 | expect(actual).to.be.an('array').which.has.lengthOf(1) 98 | }) 99 | 100 | // FIXME: not 1:1 with docs @ https://surrealdb.com/docs/surrealql/datamodel/ids 101 | // no way of using $now in create method yet. 102 | it('should support array-based record IDs.', async () => { 103 | const query = ctx.db.create("temperature:['London', time::now()]").set({ 104 | location: 'London', 105 | date: sql`string::slice(id, 24, 30)`, 106 | temperature: 23.7, 107 | }) 108 | 109 | testSurrealQl(query, { 110 | sql: "create temperature:['London', time::now()] set location = $1, date = string::slice(id, 24, 30), temperature = $2", 111 | parameters: ['London', 23.7], 112 | }) 113 | 114 | const actual = await query.execute() 115 | 116 | expect(actual).to.be.an('array').which.has.lengthOf(1) 117 | }) 118 | 119 | const idGenerationFunctions = ['rand', 'ulid', 'uuid'] 120 | 121 | idGenerationFunctions.forEach((generator) => 122 | it(`should support record IDs generated with ${generator}().`, async () => { 123 | const query = ctx.db.create(`temperature:${generator}()`).set({ 124 | time: sql`time::now()`, 125 | celsius: 37.5, 126 | }) 127 | 128 | testSurrealQl(query, { 129 | sql: `create temperature:${generator}() set time = time::now(), celsius = $1`, 130 | parameters: [37.5], 131 | }) 132 | 133 | const actual = await query.execute() 134 | 135 | expect(actual).to.be.an('array').which.has.lengthOf(1) 136 | }), 137 | ) 138 | 139 | // FIXME: can't support these due to kysely schema table name splitting @ `parseTable`. 140 | it.skip('should support simple numeric record ID ranges.', async () => { 141 | const query = ctx.db.selectFrom('person:1..1000').selectAll() 142 | 143 | testSurrealQl(query, { 144 | sql: 'select * from person:1..1000', 145 | parameters: [], 146 | }) 147 | 148 | await query.execute() 149 | }) 150 | 151 | // FIXME: can't support these due to kysely schema table name splitting @ `parseTable`. 152 | it.skip('should support array-based record ID inclusive ranges.', async () => { 153 | const query = ctx.db.selectFrom("temperature:['London', NONE]..=['London', time::now()]").selectAll() 154 | 155 | testSurrealQl(query, { 156 | sql: "select * from temperature:['London', NONE]..=['London', time::now()]", 157 | parameters: [], 158 | }) 159 | 160 | await query.execute() 161 | }) 162 | 163 | // FIXME: can't support these due to kysely schema table name splitting @ `parseTable`. 164 | it.skip('should support array-based record ID less than ranges.', async () => { 165 | const query = ctx.db.selectFrom("temperature:..['London', '2022-08-29T08:09:31']").selectAll() 166 | 167 | testSurrealQl(query, { 168 | sql: "select * from temperature:..['London', '2022-08-29T08:09:31']", 169 | parameters: [], 170 | }) 171 | 172 | await query.execute() 173 | }) 174 | 175 | // FIXME: can't support these due to kysely schema table name splitting @ `parseTable`. 176 | it.skip('should support array-based record ID greater than ranges.', async () => { 177 | const query = ctx.db.selectFrom("temperature:['London', '2022-08-29T08:03:39']..").selectAll() 178 | 179 | testSurrealQl(query, { 180 | sql: "select * from temperature:['London', '2022-08-29T08:03:39']..", 181 | parameters: [], 182 | }) 183 | 184 | await query.execute() 185 | }) 186 | 187 | // FIXME: can't support these due to kysely schema table name splitting @ `parseTable`. 188 | it.skip('should support array-based record ID ranges.', async () => { 189 | const query = ctx.db 190 | .selectFrom("temperature:['London', '2022-08-29T08:03:39']..['London', '2022-08-29T08:09:31']") 191 | .selectAll() 192 | 193 | testSurrealQl(query, { 194 | sql: "select * from temperature:['London', '2022-08-29T08:03:39']..['London', '2022-08-29T08:09:31']", 195 | parameters: [], 196 | }) 197 | 198 | await query.execute() 199 | }) 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /tests/nodejs/surreal-kysely/relate.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {sql} from 'kysely' 3 | 4 | import {DIALECTS, dropTables, initTests, prepareTables, testSurrealQl, type TestContext} from './shared' 5 | 6 | DIALECTS.forEach((dialect) => { 7 | describe(`${dialect}: SurrealKysely.relate(...)`, () => { 8 | let ctx: TestContext 9 | 10 | before(async () => { 11 | ctx = initTests(dialect) 12 | 13 | await prepareTables(ctx, ['user', 'article', 'company']) 14 | }) 15 | 16 | beforeEach(async () => { 17 | await prepareTables(ctx, ['user', 'article', 'company']) 18 | }) 19 | 20 | afterEach(async () => { 21 | await dropTables(ctx, ['write', 'like', 'company', 'user', 'article']) 22 | }) 23 | 24 | after(async () => { 25 | await ctx.db.destroy() 26 | }) 27 | 28 | it('should execute a relate...set query between two specific records.', async () => { 29 | const query = ctx.db 30 | .relate('write') 31 | .from('user:tobie') 32 | .to('article:surreal') 33 | .set({'time.written': sql`time::now()`}) 34 | 35 | testSurrealQl(query, { 36 | sql: 'relate user:tobie->write->article:surreal set time.written = time::now()', 37 | parameters: [], 38 | }) 39 | 40 | const actual = await query.execute() 41 | 42 | expect(actual).to.be.an('array').which.has.lengthOf(1) 43 | }) 44 | 45 | // FIXME: this query started failing on newer image version. posted a question in official discord server. 46 | it.skip('should execute a relate...set query between multiple specific users and devs.', async () => { 47 | const query = ctx.db 48 | .relate('like') 49 | .from(ctx.db.selectFrom('company:surrealdb').select('users')) 50 | .to( 51 | ctx.db 52 | .selectFrom('user') 53 | .where((eb) => eb.cmpr('tags', sql`contains`, sql.lit('developer'))) 54 | .selectAll(), 55 | ) 56 | .set({'time.connected': sql`time::now()`}) 57 | 58 | testSurrealQl(query, { 59 | sql: 'relate $1->like->$2 set time.connected = time::now()', 60 | parameters: [ 61 | 'SURREALQL::(select users from company:surrealdb)', 62 | "SURREALQL::(select * from user where tags contains 'developer')", 63 | ], 64 | }) 65 | 66 | const actual = await query.execute() 67 | 68 | expect(actual).to.be.an('array').that.is.not.empty 69 | }) 70 | 71 | it('should execute a relete...content query between two specific records.', async () => { 72 | const query = ctx.db 73 | .relate('write') 74 | .from('user:tobie') 75 | .to('article:surreal') 76 | .content(sql`{source: 'Apple notes', tags: ['notes', 'markdown'], time: {written: time::now()}}`) 77 | 78 | testSurrealQl(query, { 79 | sql: [ 80 | 'relate user:tobie->write->article:surreal', 81 | "content {source: 'Apple notes', tags: ['notes', 'markdown'], time: {written: time::now()}}", 82 | ], 83 | parameters: [], 84 | }) 85 | 86 | const actual = await query.execute() 87 | 88 | expect(actual).to.be.an('array').which.has.lengthOf(1) 89 | }) 90 | 91 | it('should execute a relate...set query between two specific records (table and id in separate arguments).', async () => { 92 | const query = ctx.db 93 | .relate('write') 94 | .from('user', 'tobie') 95 | .to('article', 'surrealql') 96 | .set({'time.written': sql`time::now()`}) 97 | 98 | testSurrealQl(query, { 99 | sql: 'relate user:tobie->write->article:surrealql set time.written = time::now()', 100 | parameters: [], 101 | }) 102 | 103 | const actual = await query.execute() 104 | 105 | expect(actual).to.be.an('array').which.has.lengthOf(1) 106 | }) 107 | 108 | it('should execute a relate...set...return none query between two specific records.', async () => { 109 | const query = ctx.db 110 | .relate('write') 111 | .from('user:tobie') 112 | .to('article:surreal') 113 | .set({'time.written': sql`time::now()`}) 114 | .return('none') 115 | 116 | testSurrealQl(query, { 117 | sql: 'relate user:tobie->write->article:surreal set time.written = time::now() return none', 118 | parameters: [], 119 | }) 120 | 121 | const actual = await query.execute() 122 | 123 | expect(actual).to.be.an('array').that.is.empty 124 | }) 125 | 126 | it('should execute a relate...set...return diff query between two specific records.', async () => { 127 | const query = ctx.db 128 | .relate('write') 129 | .from('user:tobie') 130 | .to('article:surreal') 131 | .set({'time.written': sql`time::now()`}) 132 | .return('diff') 133 | 134 | testSurrealQl(query, { 135 | sql: 'relate user:tobie->write->article:surreal set time.written = time::now() return diff', 136 | parameters: [], 137 | }) 138 | 139 | const actual = await query.execute() 140 | 141 | expect(actual).to.be.an('array').which.has.lengthOf(1) 142 | }) 143 | 144 | it('should execute a relate...set...return before query between two specific records.', async () => { 145 | const query = ctx.db 146 | .relate('write') 147 | .from('user:tobie') 148 | .to('article:surreal') 149 | .set({'time.written': sql`time::now()`}) 150 | .return('before') 151 | 152 | testSurrealQl(query, { 153 | sql: 'relate user:tobie->write->article:surreal set time.written = time::now() return before', 154 | parameters: [], 155 | }) 156 | 157 | const actual = await query.execute() 158 | 159 | expect(actual).to.be.an('array').which.has.lengthOf(1) 160 | }) 161 | 162 | it('should execute a relate...set...return after query between two specific records.', async () => { 163 | const query = ctx.db 164 | .relate('write') 165 | .from('user:tobie') 166 | .to('article:surreal') 167 | .set({'time.written': sql`time::now()`}) 168 | .return('after') 169 | 170 | testSurrealQl(query, { 171 | sql: 'relate user:tobie->write->article:surreal set time.written = time::now() return after', 172 | parameters: [], 173 | }) 174 | 175 | const actual = await query.execute() 176 | 177 | expect(actual).to.be.an('array').which.has.lengthOf(1) 178 | }) 179 | 180 | it('should execute a relate...set...return field query between two specific records.', async () => { 181 | const query = ctx.db 182 | .relate('write') 183 | .from('user:tobie') 184 | .to('article:surreal') 185 | .set({'time.written': sql`time::now()`}) 186 | .return('time') 187 | 188 | testSurrealQl(query, { 189 | sql: 'relate user:tobie->write->article:surreal set time.written = time::now() return time', 190 | parameters: [], 191 | }) 192 | 193 | const actual = await query.execute() 194 | 195 | expect(actual).to.be.an('array').which.has.lengthOf(1) 196 | }) 197 | 198 | it('should execute a relate...set...return multiple fields query between two specific records.', async () => { 199 | const query = ctx.db 200 | .relate('write') 201 | .from('user:tobie') 202 | .to('article:surreal') 203 | .set({source: 'Samsung notes', 'time.written': sql`time::now()`}) 204 | .return(['source', 'time']) 205 | 206 | testSurrealQl(query, { 207 | sql: 'relate user:tobie->write->article:surreal set source = $1, time.written = time::now() return source, time', 208 | parameters: ['Samsung notes'], 209 | }) 210 | 211 | const actual = await query.execute() 212 | 213 | expect(actual).to.be.an('array').which.has.lengthOf(1) 214 | }) 215 | 216 | it('should execute a relate...set query between multiple specific records and a single outbound record.', async () => { 217 | const query = ctx.db 218 | .relate('like') 219 | .from(['user:tobie', 'user:igal']) 220 | .to('user:moshe') 221 | .set({'time.connected': sql`time::now()`}) 222 | 223 | testSurrealQl(query, { 224 | sql: 'relate [user:tobie, user:igal]->like->user:moshe set time.connected = time::now()', 225 | parameters: [], 226 | }) 227 | 228 | const actual = await query.execute() 229 | 230 | expect(actual).to.be.an('array').that.has.lengthOf(2) 231 | }) 232 | 233 | it('should execute a relate...set query between a single specific record and multiple specific records.', async () => { 234 | const query = ctx.db 235 | .relate('like') 236 | .from('user:tobie') 237 | .to(['user:moshe', 'user:igal']) 238 | .set({'time.connected': sql`time::now()`}) 239 | 240 | testSurrealQl(query, { 241 | sql: 'relate user:tobie->like->[user:moshe, user:igal] set time.connected = time::now()', 242 | parameters: [], 243 | }) 244 | 245 | const actual = await query.execute() 246 | 247 | expect(actual).to.be.an('array').that.has.lengthOf(2) 248 | }) 249 | 250 | it('should execute a relate...set query between multiple specific records and multiple specific records.', async () => { 251 | const query = ctx.db 252 | .relate('write') 253 | .from(['user:tobie', 'user:igal']) 254 | .to(['article:surreal', 'article:surrealql']) 255 | .set({'time.written': sql`time::now()`}) 256 | 257 | testSurrealQl(query, { 258 | sql: 'relate [user:tobie, user:igal]->write->[article:surreal, article:surrealql] set time.written = time::now()', 259 | parameters: [], 260 | }) 261 | 262 | const actual = await query.execute() 263 | 264 | expect(actual).to.be.an('array').that.has.lengthOf(4) 265 | }) 266 | }) 267 | }) 268 | -------------------------------------------------------------------------------- /tests/nodejs/surreal-kysely/shared.ts: -------------------------------------------------------------------------------- 1 | import {expect, use} from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import {sql, type ColumnType, type Compilable, type RawBuilder} from 'kysely' 4 | import nodeFetch from 'node-fetch' 5 | import Surreal from 'surrealdb.js' 6 | import {fetch as undiciFetch} from 'undici' 7 | 8 | import {SurrealDbHttpDialect, SurrealDbWebSocketsDialect, SurrealKysely, type SurrealEdge} from '../../../src' 9 | 10 | use(chaiAsPromised) 11 | 12 | export interface Database { 13 | person: Person 14 | user: User 15 | write: SurrealEdge 16 | article: Article 17 | company: Company 18 | like: SurrealEdge 19 | account: Account 20 | temperature: Temperature 21 | } 22 | 23 | interface Person { 24 | name: string | null 25 | company: string | null 26 | skills: string[] | null 27 | username: string | null 28 | interests: string[] | null 29 | railcard: string | null 30 | age: number | null 31 | children: any[] | null 32 | newsletter: boolean | null 33 | interested: boolean | null 34 | } 35 | 36 | interface User { 37 | nickname: string | null 38 | tags: string[] | null 39 | } 40 | 41 | interface Write { 42 | source: string | null 43 | tags: string[] | null 44 | time: ColumnType< 45 | {written: string | null} | null, 46 | {written: string | RawBuilder | null} | null, 47 | {written: string | RawBuilder | null} | null 48 | > 49 | } 50 | 51 | interface Article { 52 | author: string | null 53 | time: string | null 54 | } 55 | 56 | interface Company { 57 | name: string | null 58 | users: string[] | null 59 | } 60 | 61 | interface Like { 62 | time: { 63 | connected: string | null 64 | } | null 65 | } 66 | 67 | export interface Account { 68 | name: string 69 | } 70 | 71 | interface Temperature { 72 | celsius: number | null 73 | date: string | null 74 | location: string | null 75 | temperature: number | null 76 | time: string | null 77 | } 78 | 79 | export interface TestContext { 80 | db: SurrealKysely 81 | } 82 | 83 | export const DIALECTS = ['http', 'websockets'] as const 84 | 85 | const BASE_CONFIG = { 86 | database: 'test', 87 | hostname: 'localhost:8000', 88 | namespace: 'test', 89 | password: 'root', 90 | username: 'root', 91 | } 92 | 93 | export function initTests(dialect: typeof DIALECTS[number]): TestContext { 94 | return { 95 | db: { 96 | http: () => 97 | new SurrealKysely({ 98 | dialect: new SurrealDbHttpDialect({ 99 | ...BASE_CONFIG, 100 | fetch: getFetch(), 101 | }), 102 | }), 103 | websockets: () => 104 | new SurrealKysely({ 105 | dialect: new SurrealDbWebSocketsDialect({ 106 | ...BASE_CONFIG, 107 | Driver: Surreal, 108 | }), 109 | }), 110 | }[dialect](), 111 | } 112 | } 113 | 114 | function getFetch() { 115 | const {version} = process 116 | 117 | if (version.startsWith('v18')) { 118 | return fetch 119 | } 120 | 121 | if (version.startsWith('v16')) { 122 | return undiciFetch 123 | } 124 | 125 | return nodeFetch 126 | } 127 | 128 | export function testSurrealQl(actual: Compilable, expected: {parameters: unknown[]; sql: string | string[]}): void { 129 | const {sql} = expected 130 | 131 | const compiledQuery = actual.compile() 132 | 133 | expect(compiledQuery.sql).to.be.equal(Array.isArray(sql) ? sql.join?.(' ') : sql) 134 | expect(compiledQuery.parameters).to.be.deep.equal(expected.parameters) 135 | } 136 | 137 | export async function prepareTables(ctx: TestContext, tables: ReadonlyArray): Promise { 138 | await dropTables(ctx, tables) 139 | 140 | return await tables.reduce(async (acc, table) => { 141 | switch (table) { 142 | case 'account': 143 | return acc.then(insertAccounts(ctx)) 144 | case 'article': 145 | return acc.then(insertArticles(ctx)) 146 | case 'company': 147 | return acc.then(insertCompanies(ctx)) 148 | case 'like': 149 | // return acc.then(insertLikes) 150 | return acc 151 | case 'person': 152 | return acc.then(insertPeople(ctx)) 153 | case 'user': 154 | return acc.then(insertUsers(ctx)) 155 | case 'write': 156 | // return acc.then(insertWrites) 157 | return acc 158 | default: 159 | throw new Error(`missing insertion function for ${table}!`) 160 | } 161 | }, Promise.resolve()) 162 | } 163 | 164 | export async function dropTables(ctx: TestContext, tables: ReadonlyArray) { 165 | return await tables.reduce(async (acc, table) => { 166 | return acc.then(() => dropTable(ctx, table)) 167 | }, Promise.resolve()) 168 | } 169 | 170 | async function dropTable(ctx: TestContext, table: keyof Database) { 171 | await sql`remove table ${sql.table(table)}`.execute(ctx.db) 172 | } 173 | 174 | function insertUsers(ctx: TestContext) { 175 | return async () => { 176 | await ctx.db 177 | .insertInto('user') 178 | .values([ 179 | {id: 'tobie', nickname: 'Tobie', tags: ['developer']}, 180 | {id: 'igal', nickname: 'Igal'}, 181 | {id: 'moshe', nickname: 'Moshe'}, 182 | ]) 183 | .execute() 184 | } 185 | } 186 | 187 | function insertArticles(ctx: TestContext) { 188 | return async () => { 189 | await ctx.db 190 | .insertInto('article') 191 | .values([ 192 | {id: 'surreal', title: 'Surreal'}, 193 | {id: 'surrealql', title: 'SurrealQL'}, 194 | ]) 195 | .execute() 196 | } 197 | } 198 | 199 | function insertCompanies(ctx: TestContext) { 200 | return async () => { 201 | await ctx.db 202 | .insertInto('company') 203 | .values([{id: 'surrealdb', users: sql`[user:igal]`}]) 204 | .execute() 205 | } 206 | } 207 | 208 | function insertPeople(ctx: TestContext) { 209 | return async () => { 210 | await ctx.db 211 | .insertInto('person') 212 | .values([{age: 10}, {age: 21}, {age: 30}, {age: 65}]) 213 | .execute() 214 | } 215 | } 216 | 217 | function insertAccounts(ctx: TestContext) { 218 | return async () => { 219 | await ctx.db 220 | .insertInto('account') 221 | .values([ 222 | {id: 'account:123', name: 'Account 123'}, 223 | {id: 'account:456', name: 'Account 456'}, 224 | ]) 225 | .execute() 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tests/nodejs/surreal-kysely/transaction.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {sql} from 'kysely' 3 | import {fail} from 'node:assert' 4 | 5 | import {SurrealDbHttpTransactionsUnsupportedError} from '../../../src' 6 | import {DIALECTS, dropTables, initTests, prepareTables, type TestContext} from './shared' 7 | 8 | DIALECTS.forEach((dialect) => { 9 | describe(`${dialect}: SurrealKysely.transaction(...)`, () => { 10 | let ctx: TestContext 11 | 12 | before(async () => { 13 | ctx = initTests(dialect) 14 | }) 15 | 16 | beforeEach(async () => { 17 | await prepareTables(ctx, ['person']) 18 | }) 19 | 20 | afterEach(async () => { 21 | await dropTables(ctx, ['person']) 22 | }) 23 | 24 | after(async () => { 25 | await ctx.db.destroy() 26 | }) 27 | 28 | if (dialect === 'http') { 29 | it('should throw an error when trying to use transactions.', async () => { 30 | try { 31 | await ctx.db.transaction().execute(async (_) => {}) 32 | 33 | fail('Should have thrown an error.') 34 | } catch (error) { 35 | expect(error).to.be.an.instanceOf(SurrealDbHttpTransactionsUnsupportedError) 36 | } 37 | }) 38 | } 39 | 40 | if (dialect === 'websockets') { 41 | it('should execute a transaction and commit.', async () => { 42 | await ctx.db.transaction().execute(async (tx) => { 43 | await tx.insertInto('person').values({id: 'person:transaction0', name: 'Tobie'}).execute() 44 | await tx.insertInto('person').values({id: 'person:transaction1', name: 'Igal'}).execute() 45 | }) 46 | 47 | await expect( 48 | ctx.db 49 | .selectFrom('person') 50 | .selectAll() 51 | .where('id', sql`inside`, sql`['person:transaction0', 'person:transaction1']`) 52 | .execute(), 53 | ).to.eventually.have.lengthOf(2) 54 | }) 55 | 56 | // FIXME: This test fails because the transaction is not rolled back. 57 | it.skip('should execute a transaction and cancel.', async () => { 58 | try { 59 | await ctx.db.transaction().execute(async (tx) => { 60 | await tx.insertInto('person').values({id: 'person:transaction2', name: 'Tobie'}).execute() 61 | await tx.insertInto('person').values({id: 'person:transaction3', name: 'Igal'}).execute() 62 | 63 | throw new Error('Cancel transaction.') 64 | }) 65 | 66 | fail('Should have thrown an error.') 67 | } catch (error) { 68 | expect(error.message).to.equal('Cancel transaction.') 69 | } 70 | 71 | await expect( 72 | ctx.db 73 | .selectFrom('person') 74 | .selectAll() 75 | .where('id', sql`inside`, sql`['person:transaction2', 'person:transaction3']`) 76 | .execute(), 77 | ).to.eventually.have.lengthOf(0) 78 | }) 79 | } 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules", "dist", "tests", "examples", "scripts", "assets"] 8 | } 9 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup' 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | dts: true, 6 | entry: ['./src/index.ts', './src/helpers/index.ts'], 7 | format: ['cjs', 'esm'], 8 | legacyOutput: true, 9 | outDir: 'dist', 10 | sourcemap: true, 11 | }) 12 | --------------------------------------------------------------------------------