@storecraft/database-turso 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/driver.js +33 -0
- package/index.js +4 -0
- package/kysely.turso.dialect.js +246 -0
- package/kysely.turso.utils.js +33 -0
- package/migrate.js +5 -0
- package/package.json +40 -0
- package/tests/runner.test.js +47 -0
- package/tests/sandbox.js +33 -0
- package/tsconfig.json +19 -0
- package/types.public.d.ts +16 -0
package/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# Storecraft Turso (libsql) Database support
|
2
|
+
|
3
|
+
<div style="text-align:center">
|
4
|
+
<img src='https://storecraft.app/storecraft-color.svg'
|
5
|
+
width='90%'' />
|
6
|
+
</div><hr/><br/>
|
7
|
+
|
8
|
+
Official `Turso` / `libSql` driver for `StoreCraft` on any platforms.
|
9
|
+
|
10
|
+
```bash
|
11
|
+
npm i @storecraft/database-cloudflare-d1
|
12
|
+
```
|
13
|
+
|
14
|
+
## Setup
|
15
|
+
|
16
|
+
- First, login to your [turso](https://turso.tech) account.
|
17
|
+
- Create a database.
|
18
|
+
- Create an API Key.
|
19
|
+
|
20
|
+
|
21
|
+
## usage
|
22
|
+
|
23
|
+
```js
|
24
|
+
import 'dotenv/config';
|
25
|
+
import http from "node:http";
|
26
|
+
import { join } from "node:path";
|
27
|
+
import { homedir } from "node:os";
|
28
|
+
|
29
|
+
import { App } from '@storecraft/core'
|
30
|
+
import { NodePlatform } from '@storecraft/platforms/node';
|
31
|
+
import { Turso } from '@storecraft/database-turso'
|
32
|
+
import { migrateToLatest } from '@storecraft/database-turso/migrate.js'
|
33
|
+
import { NodeLocalStorage } from '@storecraft/storage-local/node'
|
34
|
+
|
35
|
+
const app = new App(
|
36
|
+
{
|
37
|
+
auth_admins_emails: ['admin@sc.com'],
|
38
|
+
auth_secret_access_token: 'auth_secret_access_token',
|
39
|
+
auth_secret_refresh_token: 'auth_secret_refresh_token'
|
40
|
+
}
|
41
|
+
)
|
42
|
+
.withPlatform(new NodePlatform())
|
43
|
+
.withDatabase(
|
44
|
+
new Turso(
|
45
|
+
{
|
46
|
+
prefers_batch_over_transactions: true,
|
47
|
+
libsqlConfig: {
|
48
|
+
url: process.env.TURSO_URL,
|
49
|
+
authToken: process.env.TURSO_API_TOKEN,
|
50
|
+
}
|
51
|
+
}
|
52
|
+
)
|
53
|
+
)
|
54
|
+
.withStorage(new NodeLocalStorage(join(homedir(), 'tomer')))
|
55
|
+
|
56
|
+
await app.init();
|
57
|
+
await migrateToLatest(app.db, false);
|
58
|
+
|
59
|
+
const server = http.createServer(app.handler).listen(
|
60
|
+
8000,
|
61
|
+
() => {
|
62
|
+
console.log(`Server is running on http://localhost:8000`);
|
63
|
+
}
|
64
|
+
);
|
65
|
+
|
66
|
+
```
|
67
|
+
|
68
|
+
```text
|
69
|
+
Author: Tomer Shalev <tomer.shalev@gmail.com>
|
70
|
+
```
|
package/driver.js
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
import { App } from '@storecraft/core';
|
2
|
+
import { SQL } from '@storecraft/database-sql-base';
|
3
|
+
import { LibsqlDialect } from './kysely.turso.dialect.js';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* @param {any} b
|
7
|
+
* @param {string} msg
|
8
|
+
*/
|
9
|
+
const assert = (b, msg) => {
|
10
|
+
if(!Boolean(b)) throw new Error(msg);
|
11
|
+
}
|
12
|
+
|
13
|
+
|
14
|
+
/**
|
15
|
+
* @extends {SQL}
|
16
|
+
*/
|
17
|
+
export class Turso extends SQL {
|
18
|
+
|
19
|
+
/**
|
20
|
+
*
|
21
|
+
* @param {import('./types.public.d.ts').Config} [config] config
|
22
|
+
*/
|
23
|
+
constructor(config) {
|
24
|
+
super(
|
25
|
+
{
|
26
|
+
dialect_type: 'SQLITE',
|
27
|
+
dialect: new LibsqlDialect(config),
|
28
|
+
}
|
29
|
+
);
|
30
|
+
|
31
|
+
}
|
32
|
+
|
33
|
+
}
|
package/index.js
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
import * as libsql from "@libsql/client";
|
2
|
+
import * as kysely from "kysely";
|
3
|
+
|
4
|
+
/**
|
5
|
+
* @typedef {import('kysely').Driver} Driver
|
6
|
+
* @typedef {import('kysely').Dialect} Dialect
|
7
|
+
* @typedef {import('kysely').DatabaseConnection} DatabaseConnection
|
8
|
+
* @typedef {import('./types.public.d.ts').Config} Config
|
9
|
+
*/
|
10
|
+
|
11
|
+
/**
|
12
|
+
*
|
13
|
+
* @implements {Dialect}
|
14
|
+
*/
|
15
|
+
export class LibsqlDialect {
|
16
|
+
/** @type {Config} */
|
17
|
+
#config;
|
18
|
+
|
19
|
+
/**
|
20
|
+
*
|
21
|
+
* @param {Config} config
|
22
|
+
*/
|
23
|
+
constructor(config) {
|
24
|
+
this.#config = config;
|
25
|
+
}
|
26
|
+
|
27
|
+
createAdapter() { return new kysely.SqliteAdapter(); }
|
28
|
+
createQueryCompiler() { return new kysely.SqliteQueryCompiler(); }
|
29
|
+
createDriver() {
|
30
|
+
|
31
|
+
if (this.#config?.libsqlConfig?.url===undefined) {
|
32
|
+
throw new Error(
|
33
|
+
"Please specify either `client` or `url` in the LibsqlDialect config"
|
34
|
+
);
|
35
|
+
}
|
36
|
+
|
37
|
+
return new LibsqlDriver(
|
38
|
+
libsql.createClient(this.#config.libsqlConfig),
|
39
|
+
this.#config
|
40
|
+
);
|
41
|
+
}
|
42
|
+
|
43
|
+
/** @type {Dialect["createIntrospector"]} */
|
44
|
+
createIntrospector(db) {
|
45
|
+
return new kysely.SqliteIntrospector(db);
|
46
|
+
}
|
47
|
+
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
*
|
52
|
+
* @implements {Driver}
|
53
|
+
*/
|
54
|
+
export class LibsqlDriver {
|
55
|
+
/** @type {libsql.Client} */
|
56
|
+
client;
|
57
|
+
|
58
|
+
/**
|
59
|
+
* @param {libsql.Client} client
|
60
|
+
* @param {Config} config
|
61
|
+
*/
|
62
|
+
constructor(client, config) {
|
63
|
+
this.client = client;
|
64
|
+
this.config = config;
|
65
|
+
}
|
66
|
+
|
67
|
+
async init() {}
|
68
|
+
|
69
|
+
async acquireConnection() {
|
70
|
+
return new LibsqlConnection(this.client, this.config);
|
71
|
+
}
|
72
|
+
|
73
|
+
/**
|
74
|
+
*
|
75
|
+
* @param {LibsqlConnection} connection
|
76
|
+
* @param {kysely.TransactionSettings} settings
|
77
|
+
*/
|
78
|
+
async beginTransaction(connection, settings) {
|
79
|
+
await connection.beginTransaction();
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
*
|
84
|
+
* @param {LibsqlConnection} connection
|
85
|
+
*/
|
86
|
+
async commitTransaction(connection) {
|
87
|
+
await connection.commitTransaction();
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
*
|
92
|
+
* @param {LibsqlConnection} connection
|
93
|
+
*/
|
94
|
+
async rollbackTransaction(connection) {
|
95
|
+
await connection.rollbackTransaction();
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
*
|
100
|
+
* @param {LibsqlConnection} _conn
|
101
|
+
*/
|
102
|
+
async releaseConnection(_conn) {}
|
103
|
+
|
104
|
+
async destroy() {
|
105
|
+
this.client.close();
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* @implements {DatabaseConnection}
|
111
|
+
*/
|
112
|
+
export class LibsqlConnection {
|
113
|
+
/** @type {libsql.Client} */
|
114
|
+
client;
|
115
|
+
/** @type {Config} */
|
116
|
+
config;
|
117
|
+
isBatch = false;
|
118
|
+
/** @type {kysely.CompiledQuery[]} */
|
119
|
+
batch = []
|
120
|
+
|
121
|
+
/** @type {libsql.Transaction} */
|
122
|
+
#transaction;
|
123
|
+
|
124
|
+
/**
|
125
|
+
*
|
126
|
+
* @param {libsql.Client} client
|
127
|
+
* @param {Config} config
|
128
|
+
*/
|
129
|
+
constructor(client, config) {
|
130
|
+
this.client = client;
|
131
|
+
this.config = config;
|
132
|
+
}
|
133
|
+
|
134
|
+
/**
|
135
|
+
* @param {kysely.CompiledQuery[]} compiledQueries
|
136
|
+
*
|
137
|
+
* @returns {Promise<import('kysely').QueryResult<import("@libsql/client").Row>>}
|
138
|
+
*/
|
139
|
+
async #internal_executeQuery(compiledQueries) {
|
140
|
+
const target = this.#transaction ?? this.client;
|
141
|
+
|
142
|
+
const stmts = compiledQueries.map(
|
143
|
+
cq => (
|
144
|
+
{
|
145
|
+
sql: cq.sql,
|
146
|
+
args: (/** @type {import("@libsql/client").InArgs} */ (cq.parameters)),
|
147
|
+
}
|
148
|
+
)
|
149
|
+
);
|
150
|
+
|
151
|
+
const results = await target.batch(
|
152
|
+
stmts
|
153
|
+
);
|
154
|
+
|
155
|
+
// console.log('q', JSON.stringify({sql, params}, null, 2))
|
156
|
+
console.log('stmts', JSON.stringify(stmts, null, 2))
|
157
|
+
console.log('result', JSON.stringify(results, null, 2))
|
158
|
+
|
159
|
+
const last_result = results?.at(-1);
|
160
|
+
|
161
|
+
return {
|
162
|
+
insertId: last_result?.lastInsertRowid,
|
163
|
+
rows: last_result?.rows ?? [],
|
164
|
+
numAffectedRows: BigInt(last_result?.rowsAffected ?? 0),
|
165
|
+
// @ts-ignore deprecated in kysely >= 0.23, keep for backward compatibility.
|
166
|
+
numUpdatedOrDeletedRows: last_result?.rowsAffected,
|
167
|
+
};
|
168
|
+
|
169
|
+
}
|
170
|
+
|
171
|
+
/**
|
172
|
+
*
|
173
|
+
* @param {kysely.CompiledQuery} compiledQuery
|
174
|
+
*
|
175
|
+
* @returns {Promise<import('kysely').QueryResult>}
|
176
|
+
*/
|
177
|
+
async executeQuery(compiledQuery) {
|
178
|
+
console.log('this.isBatch', this.isBatch)
|
179
|
+
if(this.isBatch) {
|
180
|
+
this.batch.push(compiledQuery);
|
181
|
+
return Promise.resolve(
|
182
|
+
{
|
183
|
+
rows: []
|
184
|
+
}
|
185
|
+
)
|
186
|
+
} else {
|
187
|
+
return this.#internal_executeQuery([compiledQuery]);
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
async beginTransaction() {
|
192
|
+
if(this.config.prefers_batch_over_transactions) {
|
193
|
+
this.isBatch = true;
|
194
|
+
this.batch = [];
|
195
|
+
return;
|
196
|
+
}
|
197
|
+
|
198
|
+
if (this.#transaction) {
|
199
|
+
throw new Error("Transaction already in progress");
|
200
|
+
}
|
201
|
+
this.#transaction = await this.client.transaction();
|
202
|
+
}
|
203
|
+
|
204
|
+
async commitTransaction() {
|
205
|
+
console.log('commitTransaction')
|
206
|
+
if(this.isBatch) {
|
207
|
+
// console.trace()
|
208
|
+
await this.#internal_executeQuery(this.batch);
|
209
|
+
this.isBatch = false;
|
210
|
+
this.batch = [];
|
211
|
+
return;
|
212
|
+
}
|
213
|
+
|
214
|
+
if (!this.#transaction) {
|
215
|
+
throw new Error("No transaction to commit");
|
216
|
+
}
|
217
|
+
await this.#transaction.commit();
|
218
|
+
this.#transaction = undefined;
|
219
|
+
}
|
220
|
+
|
221
|
+
async rollbackTransaction() {
|
222
|
+
if(this.isBatch) {
|
223
|
+
this.isBatch = false;
|
224
|
+
this.batch = [];
|
225
|
+
return;
|
226
|
+
}
|
227
|
+
|
228
|
+
if (!this.#transaction) {
|
229
|
+
throw new Error("No transaction to rollback");
|
230
|
+
}
|
231
|
+
await this.#transaction.rollback();
|
232
|
+
this.#transaction = undefined;
|
233
|
+
}
|
234
|
+
/**
|
235
|
+
* @template R result type
|
236
|
+
*
|
237
|
+
* @param {kysely.CompiledQuery} compiledQuery
|
238
|
+
* @param {number} chunkSize
|
239
|
+
*
|
240
|
+
* @returns {AsyncIterableIterator<import('kysely').QueryResult<R>>}
|
241
|
+
*/
|
242
|
+
async *streamQuery(compiledQuery, chunkSize) {
|
243
|
+
throw new Error("Libsql Driver does not support streaming yet");
|
244
|
+
}
|
245
|
+
|
246
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
/**
|
3
|
+
*
|
4
|
+
* @param {string} stmt
|
5
|
+
* @param {any[] | Record<string, any>} params
|
6
|
+
*/
|
7
|
+
export const prepare_and_bind = (stmt='', params=[]) => {
|
8
|
+
const params_object = Array.isArray(params) ?
|
9
|
+
params.reduce((a, v, idx) => ({ ...a, [idx+1]: v}), {}) :
|
10
|
+
params;
|
11
|
+
|
12
|
+
let current = 0;
|
13
|
+
let result = ''
|
14
|
+
let index_run = 1;
|
15
|
+
for (let m of stmt.matchAll(/\?[0-9]*/g)) {
|
16
|
+
result += stmt.slice(current, m.index);
|
17
|
+
|
18
|
+
const match_string = m[0];
|
19
|
+
let index_access = match_string.length > 1 ?
|
20
|
+
Number(match_string.slice(1)) :
|
21
|
+
index_run;
|
22
|
+
|
23
|
+
result += "'" + params_object[index_access] + "'";
|
24
|
+
|
25
|
+
current = m.index + m[0].length;
|
26
|
+
index_run+=1;
|
27
|
+
}
|
28
|
+
|
29
|
+
result += stmt.slice(current);
|
30
|
+
|
31
|
+
return result;
|
32
|
+
}
|
33
|
+
|
package/migrate.js
ADDED
package/package.json
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
{
|
2
|
+
"name": "@storecraft/database-turso",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "`Storecraft` database driver for `Turso` (cloud sqlite)",
|
5
|
+
"license": "MIT",
|
6
|
+
"author": "Tomer Shalev (https://github.com/store-craft)",
|
7
|
+
"homepage": "https://github.com/store-craft/storecraft",
|
8
|
+
"repository": {
|
9
|
+
"type": "git",
|
10
|
+
"url": "https://github.com/store-craft/storecraft.git",
|
11
|
+
"directory": "packages/database-turso"
|
12
|
+
},
|
13
|
+
"keywords": [
|
14
|
+
"commerce",
|
15
|
+
"dashboard",
|
16
|
+
"code",
|
17
|
+
"storecraft"
|
18
|
+
],
|
19
|
+
"type": "module",
|
20
|
+
"main": "index.js",
|
21
|
+
"types": "./types.public.d.ts",
|
22
|
+
"scripts": {
|
23
|
+
"database-turso:test": "node ./tests/runner.test.js",
|
24
|
+
"database-turso:publish": "npm publish --access public"
|
25
|
+
},
|
26
|
+
"peerDependencies": {
|
27
|
+
"kysely": "*"
|
28
|
+
},
|
29
|
+
"dependencies": {
|
30
|
+
"@libsql/client": "^0.9.0",
|
31
|
+
"@storecraft/core": "^1.0.0",
|
32
|
+
"@storecraft/database-sql-base": "^1.0.0"
|
33
|
+
},
|
34
|
+
"devDependencies": {
|
35
|
+
"@storecraft/test-runner": "^1.0.0",
|
36
|
+
"@types/node": "^20.11.0",
|
37
|
+
"dotenv": "^16.3.1",
|
38
|
+
"uvu": "^0.5.6"
|
39
|
+
}
|
40
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { App } from '@storecraft/core';
|
2
|
+
import { NodePlatform } from '@storecraft/platforms/node';
|
3
|
+
import { api_index } from '@storecraft/test-runner'
|
4
|
+
import { Turso } from '../index.js';
|
5
|
+
import { migrateToLatest } from '../migrate.js';
|
6
|
+
|
7
|
+
export const create_app = async () => {
|
8
|
+
|
9
|
+
const app = new App(
|
10
|
+
{
|
11
|
+
auth_admins_emails: ['admin@sc.com'],
|
12
|
+
auth_secret_access_token: 'auth_secret_access_token',
|
13
|
+
auth_secret_refresh_token: 'auth_secret_refresh_token'
|
14
|
+
}
|
15
|
+
)
|
16
|
+
.withPlatform(new NodePlatform())
|
17
|
+
.withDatabase(
|
18
|
+
new Turso(
|
19
|
+
{
|
20
|
+
prefers_batch_over_transactions: true,
|
21
|
+
libsqlConfig: {
|
22
|
+
url: process.env.TURSO_URL,
|
23
|
+
authToken: process.env.TURSO_API_TOKEN,
|
24
|
+
}
|
25
|
+
}
|
26
|
+
)
|
27
|
+
)
|
28
|
+
|
29
|
+
return app.init();
|
30
|
+
}
|
31
|
+
|
32
|
+
async function test() {
|
33
|
+
const app = await create_app();
|
34
|
+
|
35
|
+
await migrateToLatest(app.db, false);
|
36
|
+
|
37
|
+
Object.entries(api_index).slice(0, -1).forEach(
|
38
|
+
([name, runner]) => {
|
39
|
+
runner.create(app).run();
|
40
|
+
}
|
41
|
+
);
|
42
|
+
const last_test = Object.values(api_index).at(-1).create(app);
|
43
|
+
last_test.after(async ()=>{app.db.disconnect()});
|
44
|
+
last_test.run();
|
45
|
+
}
|
46
|
+
|
47
|
+
test();
|
package/tests/sandbox.js
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
import 'dotenv/config';
|
2
|
+
import { App } from '@storecraft/core';
|
3
|
+
import { Turso } from '@storecraft/database-turso';
|
4
|
+
import { migrateToLatest } from '@storecraft/database-turso/migrate.js';
|
5
|
+
import { NodePlatform } from '@storecraft/platforms/node';
|
6
|
+
|
7
|
+
export const test = async () => {
|
8
|
+
const app = new App(
|
9
|
+
{
|
10
|
+
auth_admins_emails: ['admin@sc.com'],
|
11
|
+
auth_secret_access_token: 'auth_secret_access_token',
|
12
|
+
auth_secret_refresh_token: 'auth_secret_refresh_token'
|
13
|
+
}
|
14
|
+
)
|
15
|
+
.withPlatform(new NodePlatform())
|
16
|
+
.withDatabase(
|
17
|
+
new Turso(
|
18
|
+
{
|
19
|
+
prefers_batch_over_transactions: true,
|
20
|
+
libsqlConfig: {
|
21
|
+
url: process.env.TURSO_URL,
|
22
|
+
authToken: process.env.TURSO_API_TOKEN,
|
23
|
+
}
|
24
|
+
}
|
25
|
+
)
|
26
|
+
);
|
27
|
+
|
28
|
+
await app.init();
|
29
|
+
await migrateToLatest(app.db, false);
|
30
|
+
|
31
|
+
}
|
32
|
+
|
33
|
+
test();
|
package/tsconfig.json
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
{
|
2
|
+
"compileOnSave": false,
|
3
|
+
"compilerOptions": {
|
4
|
+
"noEmit": true,
|
5
|
+
"allowJs": true,
|
6
|
+
"checkJs": true,
|
7
|
+
"target": "ESNext",
|
8
|
+
"resolveJsonModule": true,
|
9
|
+
"moduleResolution": "NodeNext",
|
10
|
+
"module": "NodeNext",
|
11
|
+
"composite": true,
|
12
|
+
},
|
13
|
+
"include": [
|
14
|
+
"*",
|
15
|
+
"src/*",
|
16
|
+
"tests/*.js",
|
17
|
+
"d1/*"
|
18
|
+
]
|
19
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import type { Config as LibSqlConfig } from '@libsql/client'
|
2
|
+
export { Turso } from './index.js';
|
3
|
+
|
4
|
+
export type Config = {
|
5
|
+
|
6
|
+
/**
|
7
|
+
* @description Official `libsql` config
|
8
|
+
*/
|
9
|
+
libsqlConfig: LibSqlConfig
|
10
|
+
|
11
|
+
/**
|
12
|
+
* @description if `true`, transactions are converted into a non-interactive batch,
|
13
|
+
* use with caution and prefer this when transactions are non-interactive
|
14
|
+
*/
|
15
|
+
prefers_batch_over_transactions?: boolean;
|
16
|
+
}
|