@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 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,4 @@
1
+ export * from './driver.js';
2
+
3
+
4
+
@@ -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
@@ -0,0 +1,5 @@
1
+ export { migrateToLatest } from '@storecraft/database-sql-base/migrate.js';
2
+
3
+
4
+
5
+
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();
@@ -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
+ }