├── .npmrc ├── packages ├── directus-libsql │ ├── .gitignore │ ├── tsconfig.json │ ├── env.d.ts │ ├── package.json │ └── src │ │ └── index.ts └── create-directus-libsql │ ├── stubs │ ├── database │ │ └── .gitkeep │ ├── extensions │ │ └── .gitkeep │ ├── uploads │ │ └── .gitkeep │ ├── .npmrc.njk │ ├── .gitignore.njk │ ├── config.js │ ├── .editorconfig │ ├── package.json.njk │ └── .env.njk │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── src │ └── index.mts ├── docker ├── rootfs │ ├── directus │ │ ├── database │ │ │ └── .gitkeep │ │ ├── extensions │ │ │ └── .gitkeep │ │ ├── uploads │ │ │ └── .gitkeep │ │ ├── .npmrc │ │ ├── config.js │ │ └── package.json │ └── usr │ │ └── bin │ │ ├── directus │ │ └── docker-entrypoint └── Dockerfile ├── pnpm-workspace.yaml ├── examples └── example-local │ ├── .gitignore │ ├── config.js │ ├── .env │ └── package.json ├── .gitignore ├── .editorconfig ├── package.json ├── compose.yaml ├── .vscode └── launch.json ├── scripts └── release.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator = true 2 | -------------------------------------------------------------------------------- /packages/directus-libsql/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/rootfs/directus/database/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/rootfs/directus/extensions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/rootfs/directus/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/rootfs/directus/.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator = true 2 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/database/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/extensions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | !stubs/**/* 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - examples/* 3 | - packages/* 4 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/.npmrc.njk: -------------------------------------------------------------------------------- 1 | shell-emulator = true 2 | -------------------------------------------------------------------------------- /examples/example-local/.gitignore: -------------------------------------------------------------------------------- 1 | /database/ 2 | /extensions/ 3 | /uploads/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | /data 5 | 6 | /.env* 7 | 8 | **/*.tgz 9 | -------------------------------------------------------------------------------- /docker/rootfs/usr/bin/directus: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | (cd /directus && pnpm exec directus "$@") 4 | -------------------------------------------------------------------------------- /docker/rootfs/usr/bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | directus bootstrap 4 | directus start 5 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/.gitignore.njk: -------------------------------------------------------------------------------- 1 | /database 2 | /extensions 3 | /uploads 4 | /dist 5 | /node_modules 6 | -------------------------------------------------------------------------------- /examples/example-local/config.js: -------------------------------------------------------------------------------- 1 | const { withLibsql } = require('@linefusion/directus-libsql'); 2 | 3 | module.exports = withLibsql(); 4 | -------------------------------------------------------------------------------- /docker/rootfs/directus/config.js: -------------------------------------------------------------------------------- 1 | 2 | const { withLibsql } = require("@linefusion/directus-libsql"); 3 | 4 | module.exports = withLibsql(); 5 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/config.js: -------------------------------------------------------------------------------- 1 | 2 | const { withLibsql } = require("@linefusion/directus-libsql"); 3 | 4 | module.exports = withLibsql(); 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /examples/example-local/.env: -------------------------------------------------------------------------------- 1 | PUBLIC_URL="http://localhost:8055" 2 | LOG_LEVEL="trace" 3 | 4 | KEY="WmVO4lnSIVpciWHepB9v0sIhB80hEqBj" 5 | SECRET="ciWHepB9v0sIhB80hEqBjWmVO4lnSIVp" 6 | 7 | ADMIN_EMAIL="admin@example.com" 8 | ADMIN_PASSWORD="password" 9 | 10 | DB_FILENAME="./database/database.db" 11 | -------------------------------------------------------------------------------- /examples/example-local/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-local", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "directus": "CONFIG_PATH=./config.js directus" 7 | }, 8 | "dependencies": { 9 | "@linefusion/directus-libsql": "workspace:*", 10 | "directus": "^10.12.1", 11 | "libsql": "0.4.0-pre.7" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@linefusion/workspace", 3 | "version": "0.0.10", 4 | "private": true, 5 | "scripts": { 6 | "build": "pnpm -r build", 7 | "example:local": "pnpm --filter example-local directus", 8 | "release": "tsx scripts/release.ts" 9 | }, 10 | "devDependencies": { 11 | "tsx": "^4.15.7", 12 | "zx": "^8.1.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | directus: 3 | image: linefusion/directus-libsql:latest 4 | ports: 5 | - 8055:8055 6 | volumes: 7 | - ./data/database:/directus/database 8 | environment: 9 | DB_FILENAME: "/directus/database/directus.sqlite" 10 | 11 | # With Turso 12 | 13 | # DB_SYNC: "true" 14 | # DB_SYNC_URL: "libsql://-.turso.io" 15 | # DB_SYNC_PERIOD: "30" 16 | # DB_AUTH_TOKEN: "" 17 | -------------------------------------------------------------------------------- /docker/rootfs/directus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directus-project-libsql", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "directus start", 7 | "bootstrap": "directus bootstrap", 8 | "directus": "directus" 9 | }, 10 | "dependencies": { 11 | "@linefusion/directus-libsql": "^0", 12 | "directus": "^10.12.1", 13 | "libsql": ">=0.4.0-pre.0 < 0.5.0" 14 | }, 15 | "packageManager": "pnpm@9.4.0" 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/package.json.njk: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directus-project-{{ name }}", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "CONFIG_PATH=./config.js directus start", 7 | "bootstrap": "CONFIG_PATH=./config.js directus bootstrap", 8 | "directus": "CONFIG_PATH=./config.js directus" 9 | }, 10 | "dependencies": { 11 | "@linefusion/directus-libsql": "^0", 12 | "directus": "^10.12.1", 13 | "libsql": ">=0.4.0-pre.0 < 0.5.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "runtimeExecutable": "pnpm", 13 | "runtimeArgs": ["--filter", "project", "directus", "start"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowJs": true, 5 | 6 | "module": "NodeNext", 7 | "target": "ES2022", 8 | "lib": ["ES2022"], 9 | 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | 16 | "esModuleInterop": true, 17 | "skipLibCheck": true, 18 | 19 | "isolatedModules": true, 20 | "moduleResolution": "NodeNext", 21 | 22 | 23 | "outDir": "bin" 24 | }, 25 | "include": ["src/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/directus-libsql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowJs": true, 5 | 6 | "module": "NodeNext", 7 | "target": "ES2022", 8 | "lib": [ 9 | "ES2022" 10 | ], 11 | 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | 18 | "esModuleInterop": true, 19 | "skipLibCheck": true, 20 | 21 | "isolatedModules": true, 22 | "moduleDetection": "force", 23 | "moduleResolution": "NodeNext", 24 | 25 | "outDir": "dist" 26 | }, 27 | "include": ["./env.d.ts", "src/**/*"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/directus-libsql/env.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module "knex/lib/dialects/better-sqlite3" { 3 | import { Knex, Client } from "knex"; 4 | import { Database } from "libsql"; 5 | import DatabaseConstructor from "libsql"; 6 | 7 | export type Config = Record; 8 | 9 | export default class Client_BetterSQLite3 { 10 | driver: typeof DatabaseConstructor; 11 | 12 | connectionSettings: Config; 13 | 14 | connection: Database; 15 | 16 | constructor(config: Config); 17 | 18 | //async acquireRawConnection(): Promise; 19 | 20 | driver(file: string, config: Config): Database; 21 | 22 | query(conn: Database, obj: string | { sql: string }): Promise; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@linefusion/create-directus-libsql", 3 | "version": "0.0.10", 4 | "bin": { 5 | "create-directus-libsql": "./bin/index.mjs" 6 | }, 7 | "files": [ 8 | "bin", 9 | "stubs" 10 | ], 11 | "scripts": { 12 | "build": "tsc" 13 | }, 14 | "dependencies": { 15 | "inquirer": "^9.2.23", 16 | "nunjucks": "^3.2.4", 17 | "valid-filename": "^4.0.0", 18 | "validate-npm-package-name": "^5.0.1", 19 | "yargs": "^17.7.2", 20 | "zx": "^8.1.3" 21 | }, 22 | "devDependencies": { 23 | "@types/inquirer": "^9.0.7", 24 | "@types/node": "^20.14.7", 25 | "@types/nunjucks": "^3.2.6", 26 | "@types/validate-npm-package-name": "^4.0.2", 27 | "@types/yargs": "^17.0.32", 28 | "typescript": "^5.4.5" 29 | }, 30 | "publishConfig": { 31 | "access": "public" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/directus-libsql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@linefusion/directus-libsql", 3 | "version": "0.0.10", 4 | "main": "dist/index.js", 5 | "description": "Experimental integration between Directus and libsql.", 6 | "scripts": { 7 | "build": "tsc" 8 | }, 9 | "keywords": [ 10 | "directus", 11 | "directus-extension", 12 | "libsql" 13 | ], 14 | "author": "João Biondo ", 15 | "files": [ 16 | "dist" 17 | ], 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@types/node": "^20.14.6", 21 | "typescript": "^5.4.5" 22 | }, 23 | "peerDependencies": { 24 | "@directus/api": "*", 25 | "@directus/env": "*", 26 | "dotenv": "*", 27 | "knex": "*", 28 | "libsql": "*" 29 | }, 30 | "peerDependenciesMeta": { 31 | "@directus/api": { 32 | "optional": false 33 | }, 34 | "@directus/env": { 35 | "optional": false 36 | }, 37 | "dotenv": { 38 | "optional": false 39 | }, 40 | "libsql": { 41 | "optional": false 42 | }, 43 | "knex": { 44 | "optional": false 45 | } 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | ENV \ 4 | PNPM_HOME="/pnpm" \ 5 | CONFIG_PATH="/directus/config.js" \ 6 | DB_FILENAME="/directus/database/database.db" \ 7 | HOST="0.0.0.0" \ 8 | PORT=8055 \ 9 | PUBLIC_URL="http://localhost:8055" \ 10 | MAX_PAYLOAD_SIZE="100mb" \ 11 | KEY="5297cf37-b89e-4008-97bf-0312335300e2" \ 12 | SECRET="5bee40a6-141f-45f1-bdd7-76ece5aab471" \ 13 | ADMIN_EMAIL="admin@example.com" \ 14 | ADMIN_PASSWORD="password" \ 15 | LOG_LEVEL="info" \ 16 | LOG_STYLE="pretty" \ 17 | CACHE_ENABLED="true" \ 18 | CACHE_STORE="memory" \ 19 | STORAGE_LOCATIONS="local" \ 20 | STORAGE_LOCAL_DRIVER="local" \ 21 | STORAGE_LOCAL_ROOT="/directus/uploads" \ 22 | EXTENSIONS_PATH="/directus/extensions" 23 | 24 | RUN corepack enable 25 | 26 | RUN corepack prepare pnpm@latest-9 --activate 27 | RUN pnpm config set store-dir /pnpm/store 28 | 29 | WORKDIR /directus/ 30 | 31 | COPY ./rootfs/directus/package.json /directus/package.json 32 | RUN --mount=type=cache,id=pnpm,target=/pnpm pnpm install 33 | 34 | COPY ./rootfs/ / 35 | RUN \ 36 | chmod +x /usr/bin/directus && \ 37 | chmod +x /usr/bin/docker-entrypoint 38 | 39 | # Envs 40 | 41 | EXPOSE 8055 42 | 43 | VOLUME [ "/directus/uploads", "/directus/database", "/directus/extensions" ] 44 | 45 | SHELL ["/bin/bash", "-c"] 46 | 47 | CMD "/usr/bin/docker-entrypoint" 48 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/stubs/.env.njk: -------------------------------------------------------------------------------- 1 | #################################################################################################### 2 | ### Database 3 | 4 | # No need to set DB_CLIENT 5 | DB_FILENAME="./database/database.db" 6 | 7 | # Embedded replicas? 8 | # DB_SYNC="true" 9 | 10 | # Replica sync URL 11 | # DB_SYNC_URL="libsql://-.turso.io" 12 | 13 | # Sync interval (in seconds) 14 | # DB_SYNC_PERIOD="60" 15 | 16 | # Authentication token 17 | # DB_AUTH_TOKEN="" 18 | 19 | #################################################################################################### 20 | ### Server 21 | 22 | HOST="0.0.0.0" 23 | PORT=8055 24 | 25 | #################################################################################################### 26 | ### Misc 27 | 28 | PUBLIC_URL="http://localhost:8055" 29 | MAX_PAYLOAD_SIZE="100mb" 30 | 31 | KEY="{{ key }}" 32 | SECRET="{{ secret }}" 33 | 34 | ADMIN_EMAIL="{{ email }}" 35 | ADMIN_PASSWORD="{{ password }}" 36 | 37 | #################################################################################################### 38 | ### Logs 39 | 40 | LOG_LEVEL="trace" 41 | LOG_STYLE="pretty" 42 | 43 | #################################################################################################### 44 | ### Caching 45 | 46 | CACHE_ENABLED=true 47 | CACHE_STORE=memory 48 | 49 | #################################################################################################### 50 | ### File Storage 51 | 52 | STORAGE_LOCATIONS="local" 53 | STORAGE_LOCAL_DRIVER="local" 54 | STORAGE_LOCAL_ROOT="./uploads" 55 | 56 | #################################################################################################### 57 | ### Extensions 58 | 59 | EXTENSIONS_PATH="./extensions" 60 | EXTENSIONS_AUTO_RELOAD=false 61 | 62 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | 3 | import { $, cd, usePowerShell, within } from "zx"; 4 | import fsp from "node:fs/promises"; 5 | 6 | import * as os from "node:os"; 7 | import * as path from "node:path"; 8 | 9 | if (os.platform() == "win32") { 10 | usePowerShell(); 11 | } 12 | 13 | (async () => { 14 | try { 15 | const { version } = JSON.parse( 16 | (await fsp.readFile("package.json", "utf-8")).toString() 17 | ); 18 | 19 | console.clear(); 20 | console.log("Installing..."); 21 | await $`pnpm install`; 22 | 23 | console.log("Syncing versions..."); 24 | const packages = 25 | await $`pnpm list --filter ./packages/* --json --depth 0`.json(); 26 | 27 | const packageFiles = await Promise.all( 28 | packages 29 | .map((pkg: { path: string }) => path.join(pkg.path, "package.json")) 30 | .map(async (packageFile: string) => ({ 31 | path: packageFile, 32 | contents: JSON.parse(await fsp.readFile(packageFile, "utf-8")), 33 | })) 34 | ); 35 | 36 | for (const pkg of packageFiles) { 37 | pkg.contents.version = version; 38 | await fsp.writeFile( 39 | pkg.path, 40 | JSON.stringify(pkg.contents, null, 2) + `\n` 41 | ); 42 | } 43 | 44 | console.log("Building code..."); 45 | await $`pnpm -r build`; 46 | 47 | console.log("Building and pushing images..."); 48 | await within(async () => { 49 | cd("docker"); 50 | await $`docker build --push --tag linefusion/directus-libsql:latest --tag linefusion/directus-libsql:${version} .`; 51 | }); 52 | 53 | console.log("Publishing packages..."); 54 | await $`pnpm -r publish --no-git-checks`; 55 | 56 | } catch (err) { 57 | console.error("Error detected. Abording operation:\n\n", err.toString()); 58 | } 59 | })(); 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Directus with libsql 2 | 3 | Experimental/PoC support for `libsql` in `Directus`. 4 | 5 | > [!CAUTION] 6 | > This is an **experimental** project and consists in workarounds and small patches to make it work without modifying the core of Directus. 7 | > In order to make Directus work nicely with libsql, some changes need to land in both Directus and libsql first. 8 | 9 | # Running 10 | 11 | ## With Docker 12 | 13 | > Note that the published image is preconfigured to make it easier to test. 14 | 15 | `docker run --rm -it -p 8055:8055 linefusion/directus-libsql:latest` 16 | 17 | To run with Turso, set these variables: 18 | 19 | ```env 20 | DB_SYNC=true 21 | DB_SYNC_URL=libsql://-.turso.io 22 | DB_SYNC_PERIOD=60 23 | DB_AUTH_TOKEN= 24 | ``` 25 | 26 | Embedded replica will live inside `/directus/database` folder. 27 | 28 | ## Create a test project 29 | 30 | Run `npm init @linefusion/directus-libsql`. 31 | 32 | Inside the project: 33 | 34 | - `pnpm directus` is an alias to Directus CLI with the config file set. 35 | - `pnpm bootstrap` is an alias to `directus bootstrap` with the config file set. 36 | - `pnpm start` is an alias to `directus start` with the config file set. 37 | 38 | ## Manual installation 39 | 40 | Example are given using `pnpm`, port it to your own package manager of choice. 41 | 42 | ### Create a normal Directus project 43 | 44 | - Create a Directus project `pnpm create directus-project some-project` 45 | - Choose SQLite as the database 46 | - Set `./database.sqlite` as your database filename 47 | - Choose email and password 48 | 49 | ### Install libsql driver 50 | 51 | - Install these packages in your project: 52 | - `pnpm install @linefusion/directus-libsql` 53 | - `pnpm install libsql@0.4.0-pre.7` 54 | - Create a `config.js` inside of it: 55 | 56 | ```js 57 | const { withLibsql } = require("@linefusion/directus-libsql"); 58 | module.exports = withLibsql(); 59 | ``` 60 | 61 | - Configure your `.env` file or set these environment variables: 62 | - `DB_FILENAME` - where your local sqlite database will live. 63 | - `DB_SYNC` - if you want to sync changes (true/false). 64 | - `DB_SYNC_URL` - remote url to sync changes to/from (url). 65 | - `DB_SYNC_PERIOD` - period to automatically sync changes (in seconds, default 60). 66 | - `DB_AUTH_TOKEN` - token for syncing (string). 67 | 68 | - Run Directus with `CONFIG_PATH` pointing to the location fo the `config.js` you just created. 69 | 70 | ## Examples 71 | 72 | Check examples folder. Each example has an associated script in the root package. 73 | 74 | To run an example use `pnpm example:` command (it's 'an alias to `directus` cli). 75 | 76 | # Additional Work 77 | 78 | - `libsql` needs a fix on the parser in order to behave exactly like SQLite. 79 | - See issue [here](https://github.com/tursodatabase/libsql/issues/1489) 80 | 81 | - Directus needs to add support for libsql since databases are all hardcoded. 82 | - See `getDatabaseClient` function [here](https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/api/src/database/index.ts#L229) 83 | - See `getDatabase` function [here](https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/api/src/database/index.ts#L47-L50), [here](https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/api/src/database/index.ts#L84-L85) and [here](https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/api/src/database/index.ts#L113-L124) 84 | - See `createInspector` [here](https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/packages/schema/src/index.ts#L31-L33) 85 | - See `createDBconnection` [here](https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/api/src/cli/utils/create-db-connection.ts#L21) 86 | 87 | - Directus could make it easier to use `libsql` until [referenced issue](https://github.com/tursodatabase/libsql/issues/1489) in `libsql` is fixed 88 | - See `columnInfo` [here](https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/packages/schema/src/dialects/sqlite.ts#L162) 89 | 90 | - ```ts 91 | await this.knex.select('name').from('sqlite_master').whereRaw(`sql LIKE "%AUTOINCREMENT%"`) 92 | ``` 93 | 94 | to 95 | 96 | ```ts 97 | await this.knex.select('name').from('sqlite_master').whereLike('sql', '%AUTOINCREMENT%') 98 | ``` 99 | -------------------------------------------------------------------------------- /packages/directus-libsql/src/index.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | import type { Database } from "libsql"; 4 | 5 | import Client_BetterSQLite3 from "knex/lib/dialects/better-sqlite3"; 6 | import libsql from "libsql"; 7 | 8 | import * as fs from "node:fs"; 9 | import * as fsp from "node:fs/promises"; 10 | import * as path from "node:path"; 11 | 12 | function validateEnv(requiredKeys: string[]) { 13 | for (const requiredKey of requiredKeys) { 14 | if (requiredKey in process.env === false) { 15 | console.error(`ERROR: "${requiredKey}" Environment Variable is missing.`); 16 | process.exit(1); 17 | } 18 | } 19 | } 20 | 21 | class Client_Libsql extends Client_BetterSQLite3 { 22 | constructor(config: any) { 23 | super({ 24 | ...(config ?? {}), 25 | useNullAsDefault: true, 26 | }); 27 | 28 | const dirname = path.dirname(this.connectionSettings.filename); 29 | if (dirname.length > 0) { 30 | fs.mkdirSync(dirname, { 31 | recursive: true, 32 | }); 33 | } 34 | } 35 | 36 | async acquireRawConnection() { 37 | 38 | 39 | const connection = new this.driver( 40 | this.connectionSettings.filename, 41 | this.connectionSettings 42 | ); 43 | 44 | if (this.connectionSettings.sync) { 45 | connection.sync(); 46 | } 47 | 48 | // connection.pragma("journal_mode = WAL"); 49 | connection.pragma("foreign_keys = ON"); 50 | 51 | return connection; 52 | } 53 | 54 | query(conn: Database, obj: string | { sql: string }) { 55 | // Workaround for 56 | // https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/packages/schema/src/dialects/sqlite.ts#L40 57 | 58 | const remappings: Record = { 59 | 'select `name` from `sqlite_master` where sql LIKE "%AUTOINCREMENT%"': 60 | "SELECT `name` from `sqlite_master` WHERE sql LIKE '%AUTOINCREMENT%'", 61 | }; 62 | 63 | if (typeof obj === "string") { 64 | obj = remappings[obj] ?? obj; 65 | } else if (typeof obj.sql === "string") { 66 | obj.sql = remappings[obj.sql] ?? obj.sql; 67 | } 68 | 69 | return super.query(conn, obj); 70 | } 71 | 72 | _driver() { 73 | return libsql; 74 | } 75 | } 76 | 77 | Object.assign(Client_Libsql.prototype, { 78 | dialect: "sqlite3", 79 | driverName: "sqlite3", 80 | }); 81 | 82 | // Workaround for 83 | // https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/api/src/database/index.ts#L229 84 | // https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/packages/schema/src/index.ts#L31-L33 85 | 86 | overrideNames(Client_Libsql, "Client_SQLite3"); 87 | 88 | Object.defineProperty(Client_Libsql, "name", { 89 | configurable: false, 90 | enumerable: true, 91 | value: "Client_SQLite3", 92 | }); 93 | 94 | export function envWithLibsql(env: NodeJS.ProcessEnv) { 95 | const config: Record = { 96 | DB_SYNC: "false", 97 | DB_SYNC_PERIOD: "30", 98 | 99 | ...env, 100 | 101 | // Override database driver 102 | DB_CLIENT: Client_Libsql, 103 | 104 | // These need to be set because of 105 | // https://github.com/directus/directus/blob/1bdc185fbdb7541cb7b367876e70ab82eb757782/api/src/database/index.ts#L85 106 | DB_HOST: "", 107 | DB_PORT: "", 108 | DB_DATABASE: "", 109 | DB_USER: "", 110 | DB_PASSWORD: "", 111 | 112 | // For some reason Directus will go nuts with libsql. I haven't dug into it just yet. 113 | PRESSURE_LIMITER_ENABLED: "false", 114 | }; 115 | 116 | validateEnv(["DB_FILENAME"]); 117 | 118 | if (config.DB_SYNC != "false") { 119 | validateEnv(["DB_SYNC", "DB_SYNC_URL", "DB_SYNC_PERIOD"]); 120 | } 121 | 122 | Object.entries(config).forEach(([key, value]) => { 123 | if (typeof value === "function" && String(value) != "Client_SQLite3") { 124 | overrideNames(value, "function"); 125 | } 126 | config[key] = value; 127 | }); 128 | 129 | return config; 130 | } 131 | 132 | export function withLibsql(config: (env: NodeJS.ProcessEnv) => any) { 133 | return (env: NodeJS.ProcessEnv) => { 134 | return envWithLibsql(config?.(env) ?? env); 135 | }; 136 | } 137 | 138 | function overrideNames(value: any, name: string) { 139 | const property = { 140 | writable: false, 141 | configurable: false, 142 | enumerable: true, 143 | value: () => name, 144 | }; 145 | Object.defineProperties(value, { 146 | toString: property, 147 | [Symbol.toStringTag]: property, 148 | [Symbol.toPrimitive]: property, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /packages/create-directus-libsql/src/index.mts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "zx/globals"; 4 | 5 | import * as os from "node:os"; 6 | import * as fs from "node:fs"; 7 | import * as fsp from "node:fs/promises"; 8 | import * as path from "node:path"; 9 | import * as crypto from "node:crypto"; 10 | import * as url from "node:url"; 11 | 12 | import isValidFilename from "valid-filename"; 13 | import validateNpmName from "validate-npm-package-name"; 14 | import { $, which, usePowerShell } from "zx"; 15 | import inquirer from "inquirer"; 16 | import nunjucks from "nunjucks"; 17 | 18 | import yargs from "yargs/yargs"; 19 | import { hideBin } from "yargs/helpers"; 20 | 21 | const args = yargs(hideBin(process.argv)) 22 | .option("name", { 23 | type: "string", 24 | description: "Name of the package", 25 | }) 26 | .option("email", { 27 | type: "string", 28 | description: "Admin email", 29 | }) 30 | .option("password", { 31 | type: "string", 32 | description: "Admin password", 33 | }) 34 | .option("pnpm", { 35 | type: "boolean", 36 | description: "Install pnpm if not found", 37 | }) 38 | .option("bootstrap", { 39 | type: "boolean", 40 | description: "Run bootstrap after install", 41 | default: true, 42 | }); 43 | process.env.FORCE_COLOR = "1"; 44 | 45 | if (os.platform() === "win32") { 46 | usePowerShell(); 47 | } 48 | 49 | $.verbose = true; 50 | 51 | function getStubRoot() { 52 | const stubs = path.join( 53 | path.dirname(url.fileURLToPath(new URL(import.meta.url))), 54 | "../stubs" 55 | ); 56 | return stubs; 57 | } 58 | 59 | async function getStubFiles() { 60 | const files = await fsp.readdir(getStubRoot(), { 61 | recursive: true, 62 | }); 63 | 64 | return await Promise.all( 65 | files 66 | .filter((file) => { 67 | return fs.lstatSync(path.join(getStubRoot(), file)).isFile(); 68 | }) 69 | .map(async (file) => ({ 70 | target: file.replace(/\.njk$/, ""), 71 | template: file.endsWith(".njk"), 72 | directory: path.dirname(file), 73 | contents: ( 74 | await fsp.readFile(path.join(getStubRoot(), file)) 75 | ).toString(), 76 | })) 77 | ); 78 | } 79 | 80 | (async function () { 81 | const argv = await args.argv; 82 | 83 | try { 84 | await which("pnpm"); 85 | } catch (err) { 86 | let { pnpm } = await inquirer.prompt([ 87 | { 88 | when: !(argv.pnpm ?? false), 89 | name: "pnpm", 90 | message: "pnpm not found, would you like to install it with corepack?", 91 | type: "confirm", 92 | default: argv.pnpm, 93 | }, 94 | ]); 95 | 96 | pnpm = pnpm ?? argv.pnpm; 97 | 98 | if (pnpm) { 99 | await $`corepack install --global pnpm`.stdio("inherit"); 100 | } else { 101 | console.error("error: unable to proceed without pnpm."); 102 | process.exit(1); 103 | } 104 | } 105 | 106 | let { name, email, password } = await inquirer.prompt([ 107 | { 108 | when: !argv.name, 109 | name: "name", 110 | message: "What is the name of the package?", 111 | type: "input", 112 | default: path.basename(process.cwd()), 113 | async validate(input, answers) { 114 | const { validForNewPackages, warnings, errors, ...others } = 115 | validateNpmName(input); 116 | 117 | let error = 118 | errors?.[0] ?? 119 | warnings?.[0] ?? 120 | (!validForNewPackages 121 | ? `${input} is not a valid package name` 122 | : false); 123 | if (error) { 124 | return error; 125 | } 126 | 127 | if (!isValidFilename(input)) { 128 | return `"${input}" is not a valid file name.`; 129 | } 130 | if (fs.existsSync(input)) { 131 | return `"${input}" already exists. Please choose a different name.`; 132 | } 133 | return true; 134 | }, 135 | }, 136 | { 137 | when: !argv.email, 138 | name: "email", 139 | message: "Admin email?", 140 | type: "input", 141 | default: "admin@example.com", 142 | validate(input) { 143 | if (!input || !input.includes("@")) { 144 | return "Email is required."; 145 | } 146 | return true; 147 | }, 148 | }, 149 | { 150 | when: !argv.password, 151 | name: "password", 152 | message: "Admin password? [default: password]", 153 | default: "password", 154 | type: "password", 155 | validate(input) { 156 | if (!input || !input.length) { 157 | return "Password is required."; 158 | } 159 | return true; 160 | }, 161 | }, 162 | ]); 163 | 164 | name = name ?? argv.name; 165 | email = email ?? argv.email; 166 | password = password ?? argv.password; 167 | 168 | console.log(`Creating package "${name}"`); 169 | 170 | await fsp.mkdir(`./${name}`, { 171 | recursive: true, 172 | }); 173 | 174 | within(async () => { 175 | cd(`./${name}`); 176 | 177 | const files = await getStubFiles(); 178 | 179 | const key = crypto.randomUUID(); 180 | const secret = crypto.randomUUID(); 181 | 182 | for (const file of files) { 183 | await fsp.mkdir(file.directory, { 184 | recursive: true, 185 | }); 186 | 187 | const contents = nunjucks.renderString(file.contents, { 188 | key, 189 | secret, 190 | name, 191 | email, 192 | password, 193 | }); 194 | 195 | console.log(`Writing ${file.target.replace(/\\/g, "/")}`); 196 | await fsp.writeFile(file.target, contents); 197 | } 198 | 199 | await $`pnpm install`.stdio("inherit"); 200 | if (argv.bootstrap) { 201 | await $`pnpm bootstrap`.stdio("inherit"); 202 | } 203 | }); 204 | 205 | console.log(""); 206 | console.log("Done!"); 207 | })(); 208 | --------------------------------------------------------------------------------