@storecraft/database-turso 1.0.8 → 1.0.10

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 CHANGED
@@ -12,7 +12,10 @@ npm i @storecraft/database-turso
12
12
  ```
13
13
 
14
14
  ## Setup
15
+ You can run a local database,
16
+ or,
15
17
 
18
+ connect to a cloud `libsql` and `Turso` platform
16
19
  - First, login to your [turso](https://turso.tech) account.
17
20
  - Create a database.
18
21
  - Create an API Key.
@@ -41,10 +44,11 @@ const app = new App(
41
44
  new Turso(
42
45
  {
43
46
  prefers_batch_over_transactions: true,
44
- libsqlConfig: {
45
- url: process.env.TURSO_URL,
46
- authToken: process.env.TURSO_API_TOKEN,
47
- }
47
+ // all of these configurations can be inferred by env variables at init
48
+ url: process.env.LIBSQL_URL,
49
+ authToken: process.env.LIBSQL_API_TOKEN,
50
+ // or local
51
+ url: 'file:local.db',
48
52
  }
49
53
  )
50
54
  )
package/index.js CHANGED
@@ -1,23 +1,51 @@
1
+ /**
2
+ * @import { Config } from './types.public.js';
3
+ * @import { ENV } from '@storecraft/core';
4
+ */
5
+
1
6
  import { SQL } from '@storecraft/database-sql-base';
2
7
  import { LibsqlDialect } from './kysely.turso.dialect.js';
3
8
 
9
+ export { LibSQLVectorStore } from './vector-store/index.js'
10
+
4
11
  /**
5
12
  * @extends {SQL}
6
13
  */
7
14
  export class Turso extends SQL {
8
15
 
16
+ /** @satisfies {ENV<Config>} */
17
+ static EnvConfig = /** @type{const} */ ({
18
+ authToken: 'LIBSQL_AUTH_TOKEN',
19
+ url: 'LIBSQL_URL'
20
+ });
21
+
9
22
  /**
10
23
  *
11
- * @param {import('./types.public.d.ts').Config} [config] config
24
+ * @param {Config} [config] config
12
25
  */
13
- constructor(config) {
26
+ constructor(config={}) {
14
27
  super(
15
28
  {
16
29
  dialect_type: 'SQLITE',
17
- dialect: new LibsqlDialect(config),
30
+ dialect: new LibsqlDialect(
31
+ {
32
+ ...config,
33
+ prefers_batch_over_transactions: config.prefers_batch_over_transactions ?? true
34
+ }
35
+ ),
18
36
  }
19
37
  );
20
-
21
38
  }
39
+
22
40
 
41
+ /** @type {SQL["init"]} */
42
+ init = (app) => {
43
+ const dialect = /** @type {LibsqlDialect}*/ (this.config.dialect);
44
+ const dconfig = dialect.config;
45
+
46
+ dconfig.authToken ??= app.platform.env[Turso.EnvConfig.authToken];
47
+ dconfig.url ??= app.platform.env[Turso.EnvConfig.url];
48
+
49
+ super.init(app);
50
+ }
23
51
  }
package/jsconfig.json CHANGED
@@ -9,6 +9,6 @@
9
9
  "*",
10
10
  "src/*",
11
11
  "tests/*.js",
12
- "d1/*"
12
+ "vector-store/*"
13
13
  ]
14
14
  }
@@ -1,12 +1,10 @@
1
- import * as libsql from "@libsql/client";
2
- import * as kysely from "kysely";
3
-
4
1
  /**
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
2
+ * @import { Driver, Dialect, DatabaseConnection, QueryResult } from 'kysely';
3
+ * @import { Config } from './types.public.js';
4
+ * @import { Row, InArgs } from '@libsql/client';
9
5
  */
6
+ import * as libsql from "@libsql/client";
7
+ import * as kysely from "kysely";
10
8
 
11
9
  /**
12
10
  *
@@ -24,18 +22,23 @@ export class LibsqlDialect {
24
22
  this.#config = config;
25
23
  }
26
24
 
25
+ get config() {
26
+ return this.#config;
27
+ }
28
+
27
29
  createAdapter() { return new kysely.SqliteAdapter(); }
28
30
  createQueryCompiler() { return new kysely.SqliteQueryCompiler(); }
29
31
  createDriver() {
30
32
 
31
- if (this.#config?.libsqlConfig?.url===undefined) {
33
+ if (this.#config?.url===undefined) {
32
34
  throw new Error(
33
35
  "Please specify either `client` or `url` in the LibsqlDialect config"
34
36
  );
35
37
  }
36
38
 
37
39
  return new LibsqlDriver(
38
- libsql.createClient(this.#config.libsqlConfig),
40
+ // @ts-ignore
41
+ libsql.createClient(this.#config),
39
42
  this.#config
40
43
  );
41
44
  }
@@ -134,16 +137,17 @@ export class LibsqlConnection {
134
137
  /**
135
138
  * @param {kysely.CompiledQuery[]} compiledQueries
136
139
  *
137
- * @returns {Promise<import('kysely').QueryResult<import("@libsql/client").Row>>}
140
+ * @returns {Promise<QueryResult<Row>>}
138
141
  */
139
142
  async #internal_executeQuery(compiledQueries) {
143
+ // console.log(compiledQueries)
140
144
  const target = this.#transaction ?? this.client;
141
145
 
142
146
  const stmts = compiledQueries.map(
143
147
  cq => (
144
148
  {
145
149
  sql: cq.sql,
146
- args: (/** @type {import("@libsql/client").InArgs} */ (cq.parameters)),
150
+ args: (/** @type {InArgs} */ (cq.parameters)),
147
151
  }
148
152
  )
149
153
  );
@@ -153,8 +157,8 @@ export class LibsqlConnection {
153
157
  );
154
158
 
155
159
  // 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))
160
+ // console.log('stmts', JSON.stringify(stmts, null, 2))
161
+ // console.log('result', JSON.stringify(results, null, 2))
158
162
 
159
163
  const last_result = results?.at(-1);
160
164
 
@@ -172,10 +176,10 @@ export class LibsqlConnection {
172
176
  *
173
177
  * @param {kysely.CompiledQuery} compiledQuery
174
178
  *
175
- * @returns {Promise<import('kysely').QueryResult>}
179
+ * @returns {Promise<QueryResult>}
176
180
  */
177
181
  async executeQuery(compiledQuery) {
178
- console.log('this.isBatch', this.isBatch)
182
+ // console.log('this.isBatch', this.isBatch)
179
183
  if(this.isBatch) {
180
184
  this.batch.push(compiledQuery);
181
185
  return Promise.resolve(
@@ -202,7 +206,7 @@ export class LibsqlConnection {
202
206
  }
203
207
 
204
208
  async commitTransaction() {
205
- console.log('commitTransaction')
209
+ // console.log('commitTransaction')
206
210
  if(this.isBatch) {
207
211
  // console.trace()
208
212
  await this.#internal_executeQuery(this.batch);
@@ -237,7 +241,7 @@ export class LibsqlConnection {
237
241
  * @param {kysely.CompiledQuery} compiledQuery
238
242
  * @param {number} chunkSize
239
243
  *
240
- * @returns {AsyncIterableIterator<import('kysely').QueryResult<R>>}
244
+ * @returns {AsyncIterableIterator<QueryResult<R>>}
241
245
  */
242
246
  async *streamQuery(compiledQuery, chunkSize) {
243
247
  throw new Error("Libsql Driver does not support streaming yet");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storecraft/database-turso",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "`Storecraft` database driver for `Turso` (cloud sqlite)",
5
5
  "license": "MIT",
6
6
  "author": "Tomer Shalev (https://github.com/store-craft)",
@@ -24,6 +24,10 @@
24
24
  "import": "./index.js",
25
25
  "types": "./types.public.d.ts"
26
26
  },
27
+ "./vector-store": {
28
+ "import": "./vector-store/index.js",
29
+ "types": "./vector-store/types.d.ts"
30
+ },
27
31
  "./*": {
28
32
  "import": "./*"
29
33
  }
@@ -18,11 +18,8 @@ export const create_app = async () => {
18
18
  .withDatabase(
19
19
  new Turso(
20
20
  {
21
+ url: ':memory:',
21
22
  prefers_batch_over_transactions: true,
22
- libsqlConfig: {
23
- url: process.env.TURSO_URL,
24
- authToken: process.env.TURSO_API_TOKEN,
25
- }
26
23
  }
27
24
  )
28
25
  );
package/tests/sandbox.js CHANGED
@@ -17,10 +17,6 @@ export const test = async () => {
17
17
  new Turso(
18
18
  {
19
19
  prefers_batch_over_transactions: true,
20
- libsqlConfig: {
21
- url: process.env.TURSO_URL,
22
- authToken: process.env.TURSO_API_TOKEN,
23
- }
24
20
  }
25
21
  )
26
22
  );
package/types.public.d.ts CHANGED
@@ -1,16 +1,29 @@
1
1
  import type { Config as LibSqlConfig } from '@libsql/client'
2
- export { Turso } from './index.js';
2
+ export * from './index.js';
3
3
 
4
- export type Config = {
4
+ export type Config = Partial<Omit<LibSqlConfig, 'url' | 'authToken'>> & {
5
5
 
6
- /**
7
- * @description Official `libsql` config
6
+ /** The database URL.
7
+ *
8
+ * The client supports `libsql:`, `http:`/`https:`, `ws:`/`wss:` and `file:` URL. For more infomation,
9
+ * please refer to the project README:
10
+ *
11
+ * https://github.com/libsql/libsql-client-ts#supported-urls
12
+ *
13
+ * If missing, it will be inferred by env variable `LIBSQL_URL`
14
+ */
15
+ url?: string;
16
+ /**
17
+ * Authentication token for the database. Not applicable for `url`=`file:local.db`.
18
+ *
19
+ * If missing, it will be inferred by env variable `LIBSQL_AUTH_TOKEN`
8
20
  */
9
- libsqlConfig: LibSqlConfig
21
+ authToken?: string;
10
22
 
11
23
  /**
12
24
  * @description if `true`, transactions are converted into a non-interactive batch,
13
25
  * use with caution and prefer this when transactions are non-interactive
26
+ * @default true
14
27
  */
15
28
  prefers_batch_over_transactions?: boolean;
16
29
  }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * @import {
3
+ * AIEmbedder, VectorStore
4
+ * } from '@storecraft/core/ai/core/types.private.js'
5
+ * @import { ENV } from '@storecraft/core';
6
+ * @import {
7
+ * Config
8
+ * } from './types.js'
9
+ * @import {
10
+ * VectorDocumentUpsert
11
+ * } from './types.private.js'
12
+ * @import { InArgs } from '@libsql/client';
13
+ */
14
+
15
+ import * as libsql from "@libsql/client";
16
+ import {
17
+ truncate_or_pad_vector
18
+ } from "@storecraft/core/ai/models/vector-stores/index.js";
19
+
20
+ export const DEFAULT_INDEX_NAME = 'vector_store';
21
+
22
+ /** @param {any} json */
23
+ const parse_json_safely = json => {
24
+ try {
25
+ return JSON.parse(json);
26
+ } catch (e) {
27
+ return {};
28
+ } finally {
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Implementation referenes:
34
+ * - https://docs.turso.tech/features/ai-and-embeddings#vectors-usage
35
+ * - https://github.com/langchain-ai/langchainjs/blob/9dfaae7e36a1ddce586b9c44fb96785fa38b36ec/libs/langchain-community/src/vectorstores/libsql.ts
36
+ */
37
+
38
+ /**
39
+ * @typedef {VectorStore} Impl
40
+ */
41
+
42
+ /**
43
+ * @description LibSQL / Turso Vector Store
44
+ *
45
+ * @implements {VectorStore}
46
+ */
47
+ export class LibSQLVectorStore {
48
+
49
+ /** @satisfies {ENV<Config>} */
50
+ static EnvConfig = /** @type{const} */ ({
51
+ authToken: 'LIBSQL_VECTOR_AUTH_TOKEN',
52
+ url: 'LIBSQL_VECTOR_URL',
53
+ });
54
+
55
+ /** @type {Config} */
56
+ config;
57
+
58
+ /** @type {libsql.Client} */
59
+ #client
60
+
61
+ /**
62
+ *
63
+ * @param {Config} config
64
+ */
65
+ constructor(config) {
66
+ this.config = {
67
+ index_name: DEFAULT_INDEX_NAME,
68
+ similarity: 'cosine',
69
+ dimensions: 1536,
70
+ ...config,
71
+ };
72
+ }
73
+
74
+ get client() {
75
+ if(!this.config.url) {
76
+ throw new Error('LibSQLVectorStore::client() - missing url');
77
+ }
78
+
79
+ // @ts-ignore
80
+ this.#client = this.#client ?? libsql.createClient(this.config);
81
+
82
+ return this.#client;
83
+ }
84
+
85
+ get index_name() {
86
+ return this.config.index_name;
87
+ }
88
+
89
+ get table_name() {
90
+ return `${this.index_name}_table`;
91
+ }
92
+
93
+ /** @type {VectorStore["onInit"]} */
94
+ onInit = (app) => {
95
+ this.config.authToken ??= app.platform.env[
96
+ LibSQLVectorStore.EnvConfig.authToken ?? 'LIBSQL_AUTH_TOKEN'
97
+ ];
98
+ this.config.url ??= app.platform.env[
99
+ LibSQLVectorStore.EnvConfig.url ?? 'LIBSQL_URL'
100
+ ];
101
+ }
102
+
103
+ /** @type {VectorStore["embedder"]} */
104
+ get embedder() {
105
+ return this.config.embedder
106
+ }
107
+
108
+ // (id TEXT, metadata TEXT, pageContent Text, updated_at TEXT, namespace TEXT, embedding F32_BLOB
109
+ /** @type {VectorStore["upsertVectors"]} */
110
+ upsertVectors = async (vectors, documents, options) => {
111
+
112
+ const updated_at = new Date().toISOString();
113
+ /** @type {VectorDocumentUpsert[]} */
114
+ const docs_upsert = documents.map(
115
+ (doc, ix) => (
116
+ {
117
+ embedding: `[${truncate_or_pad_vector(vectors[ix], this.config.dimensions).join(',')}]`,
118
+ id: doc.id,
119
+ metadata: JSON.stringify(doc.metadata ?? {}),
120
+ pageContent: doc.pageContent,
121
+ updated_at,
122
+ namespace: doc.namespace,
123
+ }
124
+ )
125
+ );
126
+
127
+ /** @type {import("@libsql/client").InStatement[]} */
128
+ const stmts_delete = docs_upsert.map(
129
+ (doc, ix) => (
130
+ {
131
+ sql: `DELETE FROM ${this.table_name} WHERE id=?`,
132
+ args: [doc.id]
133
+ }
134
+ )
135
+ );
136
+
137
+ /** @type {import("@libsql/client").InStatement[]} */
138
+ const stmts_insert = docs_upsert.map(
139
+ (doc, ix) => (
140
+ {
141
+ sql: `
142
+ INSERT INTO ${this.table_name} (id, metadata, pageContent, updated_at, namespace, embedding)
143
+ VALUES (:id, :metadata, :pageContent, :updated_at, :namespace, vector(:embedding))
144
+ `,
145
+ args: doc
146
+ }
147
+ )
148
+ );
149
+
150
+ const result = await this.client.batch(
151
+ [
152
+ ...stmts_delete,
153
+ ...stmts_insert,
154
+ ]
155
+ );
156
+
157
+ }
158
+
159
+ /** @type {VectorStore["upsertDocuments"]} */
160
+ upsertDocuments = async (documents, options) => {
161
+ // first, generate embeddings for the documents
162
+ const result = await this.embedder.generateEmbeddings(
163
+ {
164
+ content: documents.map(
165
+ doc => (
166
+ {
167
+ content: doc.pageContent,
168
+ type: 'text'
169
+ }
170
+ )
171
+ )
172
+ }
173
+ );
174
+
175
+ const vectors = result.content;
176
+
177
+ // console.log(vectors)
178
+
179
+ return this.upsertVectors(
180
+ vectors, documents, options
181
+ )
182
+ }
183
+
184
+ /** @type {VectorStore["delete"]} */
185
+ delete = async (ids) => {
186
+ await this.client.execute(
187
+ {
188
+ sql: `DELETE FROM ${this.table_name} WHERE id IN (${ids.map(id => '?').join(',')})`,
189
+ args: ids
190
+ }
191
+ );
192
+ }
193
+
194
+ /** @type {VectorStore["similaritySearch"]} */
195
+ similaritySearch = async (query, k, namespaces) => {
196
+
197
+ const embedding_result = await this.embedder.generateEmbeddings(
198
+ {
199
+ content: [
200
+ {
201
+ content: query,
202
+ type: 'text'
203
+ }
204
+ ]
205
+ }
206
+ );
207
+ const vector = truncate_or_pad_vector(
208
+ embedding_result.content[0], this.config.dimensions
209
+ );
210
+ const vector_sql_value = `[${vector.join(',')}]`
211
+ const distance_fn = this.config.similarity==='cosine' ? 'vector_distance_cos' : 'vector_distance_l2'
212
+ // SELECT title, year
213
+ // FROM vector_top_k('movies_idx', vector32('[0.064, 0.777, 0.661, 0.687]'), 3)
214
+ // JOIN movies ON movies.rowid = id
215
+ // WHERE year >= 2020;
216
+ const table = this.table_name;
217
+ const index_name = this.index_name;
218
+ /** @type {InArgs} */
219
+ let args = [];
220
+ let sql = `
221
+ SELECT id, metadata, pageContent, updated_at, namespace, ${distance_fn}(embedding, vector(?)) AS score
222
+ FROM vector_top_k('${index_name}', vector(?), ?) as top_k_view
223
+ JOIN ${table} ON ${table}.rowid = top_k_view.id
224
+ `;
225
+ args.push(vector_sql_value, vector_sql_value, k);
226
+
227
+ if(Array.isArray(namespaces) && namespaces.length) {
228
+ sql += `\nWHERE namespace IN (${namespaces.map(n => '?').join(',')})`
229
+ args.push(...namespaces);
230
+ }
231
+
232
+ sql += `
233
+ ORDER BY
234
+ ${distance_fn}(embedding, vector(?))
235
+ ASC;
236
+ `
237
+ args.push(vector_sql_value);
238
+
239
+
240
+ const result = await this.client.execute({ sql, args });
241
+
242
+ return result.rows.map(
243
+ (row) => (
244
+ {
245
+ document: {
246
+ pageContent: String(row.pageContent),
247
+ id: String(row.id),
248
+ metadata: parse_json_safely(row.metadata),
249
+ namespace: String(row.namespace),
250
+ },
251
+ score: Number(row.score)
252
+ }
253
+ )
254
+ );
255
+
256
+ }
257
+
258
+
259
+ /**
260
+ *
261
+ * @param {boolean} [delete_index_if_exists_before=false]
262
+ * @returns {Promise<boolean>}
263
+ */
264
+ createVectorIndex = async (delete_index_if_exists_before=false) => {
265
+
266
+ /** @type {string[]} */
267
+ const batch = [];
268
+
269
+ if(delete_index_if_exists_before) {
270
+ await this.deleteVectorIndex();
271
+ }
272
+
273
+ batch.push(
274
+ `CREATE TABLE IF NOT EXISTS ${this.table_name} (id TEXT, metadata TEXT, pageContent Text, updated_at TEXT, namespace TEXT, embedding F32_BLOB(${this.config.dimensions}));`,
275
+ `CREATE INDEX IF NOT EXISTS ${this.index_name} ON ${this.table_name}(libsql_vector_idx(embedding));`
276
+ );
277
+
278
+ const result = await this.client.batch(batch);
279
+ return true;
280
+ }
281
+
282
+ /**
283
+ *
284
+ * @returns {Promise<boolean>}
285
+ */
286
+ deleteVectorIndex = async () => {
287
+
288
+ /** @type {string[]} */
289
+ const batch = [];
290
+
291
+ batch.push(
292
+ `DROP INDEX IF EXISTS ${this.index_name}`,
293
+ `DROP TABLE IF EXISTS ${this.table_name}`,
294
+ );
295
+
296
+ const result = await this.client.batch(batch);
297
+ return true;
298
+ }
299
+
300
+
301
+ }
302
+
@@ -0,0 +1,48 @@
1
+
2
+ import type { AIEmbedder } from '@storecraft/core/ai';
3
+ export * from './index.js';
4
+
5
+ export type Config = {
6
+ /**
7
+ * @description The database URL.
8
+ *
9
+ * The client supports `libsql:`, `http:`/`https:`, `ws:`/`wss:` and `file:` URL. For more infomation,
10
+ * please refer to the project README:
11
+ *
12
+ * https://github.com/libsql/libsql-client-ts#supported-urls
13
+ *
14
+ * If missing, it will be inferred by env variable `LIBSQL_VECTOR_URL` or `LIBSQL_URL`
15
+ */
16
+ url?: string;
17
+ /**
18
+ * @description Authentication token for the database. Not applicable for local `url`=`file:local.db`.
19
+ *
20
+ * If missing, it will be inferred by env variable `LIBSQL_VECTOR_AUTH_TOKEN` or `LIBSQL_AUTH_TOKEN`
21
+ *
22
+ * @default ENV variable `LIBSQL_AUTH_TOKEN`
23
+ */
24
+ authToken?: string;
25
+
26
+ /**
27
+ * @description The name of the index
28
+ * @default 'vector_store'
29
+ */
30
+ index_name?: string,
31
+
32
+ /**
33
+ * @description The dimensions of the vectors to be inserted in the index.
34
+ * @default 1536
35
+ */
36
+ dimensions?: number,
37
+
38
+ /**
39
+ * @description The similiarity metric
40
+ * @default 'cosine'
41
+ */
42
+ similarity?: 'euclidean' | 'cosine',
43
+
44
+ /**
45
+ * @description Embedding model provider
46
+ */
47
+ embedder: AIEmbedder
48
+ }
@@ -0,0 +1,16 @@
1
+
2
+ export type VectorDocument = {
3
+ id: string,
4
+ metadata: Record<string, any>,
5
+ embedding: number[],
6
+ pageContent: string,
7
+ updated_at: string,
8
+ score?: number
9
+ namespace?: string
10
+ }
11
+
12
+ export type VectorDocumentUpsert = Omit<VectorDocument, 'embedding' | 'metadata' | 'score'> & {
13
+ embedding: string,
14
+ metadata: string,
15
+ }
16
+