@dotteamdev/sensdb 0.1.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 +89 -0
- package/dist/sens.d.ts +39 -0
- package/dist/sens.js +56 -0
- package/package.json +42 -0
- package/sens.ts +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Sens — TypeScript client
|
|
2
|
+
|
|
3
|
+
A zero-dependency `fetch` client for a `sens serve` HTTP/JSON endpoint. Works in
|
|
4
|
+
Node 18+, Bun, Deno, and any runtime with a global `fetch`.
|
|
5
|
+
|
|
6
|
+
> **Server-side only.** Keep the token on the server (Next.js route handlers /
|
|
7
|
+
> server actions, NestJS providers). Never ship it to the browser. Always pass
|
|
8
|
+
> user input via `params` — never string-concatenate SQL.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
Copy `sens.ts` into your project (an npm package with prebuilds is on the
|
|
13
|
+
roadmap). Start a server:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
sens serve app.db --port 8080 --token "$(openssl rand -hex 16)"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createClient } from "./sens";
|
|
21
|
+
|
|
22
|
+
const db = createClient(process.env.SENS_URL!, { token: process.env.SENS_TOKEN });
|
|
23
|
+
|
|
24
|
+
// parameterized query — user input goes in params, never in the SQL string
|
|
25
|
+
const { columns, rows } = await db.query(
|
|
26
|
+
"SELECT id, title FROM docs WHERE author = ?",
|
|
27
|
+
[author],
|
|
28
|
+
);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Next.js (App Router — server side)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// app/api/search/route.ts
|
|
35
|
+
import { NextResponse } from "next/server";
|
|
36
|
+
import { createClient } from "@/lib/sens";
|
|
37
|
+
|
|
38
|
+
const db = createClient(process.env.SENS_URL!, { token: process.env.SENS_TOKEN });
|
|
39
|
+
|
|
40
|
+
export async function POST(req: Request) {
|
|
41
|
+
const { embedding } = await req.json(); // number[] from your embedding model
|
|
42
|
+
// AI-native semantic search: nearest documents to the query vector
|
|
43
|
+
const { rows } = await db.query(
|
|
44
|
+
"SELECT title FROM docs ORDER BY embed <-> ? LIMIT 5",
|
|
45
|
+
[embedding],
|
|
46
|
+
);
|
|
47
|
+
return NextResponse.json({ results: rows.map((r) => r[0]) });
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## NestJS (a provider)
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// sens.service.ts
|
|
55
|
+
import { Injectable } from "@nestjs/common";
|
|
56
|
+
import { createClient, SensClient } from "./sens";
|
|
57
|
+
|
|
58
|
+
@Injectable()
|
|
59
|
+
export class SensService {
|
|
60
|
+
private db: SensClient = createClient(process.env.SENS_URL!, {
|
|
61
|
+
token: process.env.SENS_TOKEN,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async docsByAuthor(author: string) {
|
|
65
|
+
const { rows } = await this.db.query(
|
|
66
|
+
"SELECT id, title FROM docs WHERE author = ? ORDER BY id",
|
|
67
|
+
[author],
|
|
68
|
+
);
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async semanticSearch(embedding: number[], k = 5) {
|
|
73
|
+
const { rows } = await this.db.query(
|
|
74
|
+
"SELECT title FROM docs ORDER BY embed <-> ? LIMIT ?",
|
|
75
|
+
[embedding, k],
|
|
76
|
+
);
|
|
77
|
+
return rows.map((r) => r[0]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
- `createClient(baseUrl, { token?, fetch? })` → `SensClient`
|
|
85
|
+
- `client.query(sql, params?)` → `Promise<{ columns, rows }>` — throws `SensError` on failure
|
|
86
|
+
- `client.health()` → `Promise<{ status, version }>`
|
|
87
|
+
|
|
88
|
+
Value mapping (JSON ↔ Sens): number ↔ INTEGER/REAL, string ↔ TEXT,
|
|
89
|
+
boolean ↔ BOOLEAN, null ↔ NULL, `number[]` ↔ VECTOR.
|
package/dist/sens.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sens TypeScript client — zero dependencies, uses only `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Talks to a `sens serve` HTTP/JSON endpoint. Server-side only: keep the token
|
|
5
|
+
* on the server (Next.js route handlers / server actions, NestJS providers) and
|
|
6
|
+
* never expose it to the browser. Always pass user input as `params` — never
|
|
7
|
+
* concatenate it into the SQL string.
|
|
8
|
+
*/
|
|
9
|
+
export type SensValue = number | string | boolean | null | number[];
|
|
10
|
+
export interface SensResult {
|
|
11
|
+
/** Column names, in order. Empty for non-SELECT statements. */
|
|
12
|
+
columns: string[];
|
|
13
|
+
/** Result rows; each row is an array of values aligned with `columns`. */
|
|
14
|
+
rows: SensValue[][];
|
|
15
|
+
}
|
|
16
|
+
export declare class SensError extends Error {
|
|
17
|
+
readonly status?: number | undefined;
|
|
18
|
+
constructor(message: string, status?: number | undefined);
|
|
19
|
+
}
|
|
20
|
+
export interface SensClient {
|
|
21
|
+
/** Run a (parameterized) SQL statement. `?` placeholders are filled from `params`. */
|
|
22
|
+
query(sql: string, params?: SensValue[]): Promise<SensResult>;
|
|
23
|
+
/** Server health + version. */
|
|
24
|
+
health(): Promise<{
|
|
25
|
+
status: string;
|
|
26
|
+
version: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export interface SensClientOptions {
|
|
30
|
+
/** Bearer token, if the server was started with --token. */
|
|
31
|
+
token?: string;
|
|
32
|
+
/** Optional custom fetch (e.g. for tests or a proxy). Defaults to global fetch. */
|
|
33
|
+
fetch?: typeof fetch;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a Sens client bound to a `sens serve` base URL, e.g.
|
|
37
|
+
* const db = createClient("http://127.0.0.1:8080", { token: process.env.SENS_TOKEN });
|
|
38
|
+
*/
|
|
39
|
+
export declare function createClient(baseUrl: string, opts?: SensClientOptions): SensClient;
|
package/dist/sens.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sens TypeScript client — zero dependencies, uses only `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Talks to a `sens serve` HTTP/JSON endpoint. Server-side only: keep the token
|
|
5
|
+
* on the server (Next.js route handlers / server actions, NestJS providers) and
|
|
6
|
+
* never expose it to the browser. Always pass user input as `params` — never
|
|
7
|
+
* concatenate it into the SQL string.
|
|
8
|
+
*/
|
|
9
|
+
export class SensError extends Error {
|
|
10
|
+
constructor(message, status) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.name = "SensError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a Sens client bound to a `sens serve` base URL, e.g.
|
|
18
|
+
* const db = createClient("http://127.0.0.1:8080", { token: process.env.SENS_TOKEN });
|
|
19
|
+
*/
|
|
20
|
+
export function createClient(baseUrl, opts = {}) {
|
|
21
|
+
const url = baseUrl.replace(/\/+$/, "");
|
|
22
|
+
const doFetch = opts.fetch ?? fetch;
|
|
23
|
+
const headers = { "content-type": "application/json" };
|
|
24
|
+
if (opts.token) {
|
|
25
|
+
headers["authorization"] = `Bearer ${opts.token}`;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
async query(sql, params = []) {
|
|
29
|
+
const res = await doFetch(`${url}/query`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers,
|
|
32
|
+
body: JSON.stringify({ sql, params }),
|
|
33
|
+
});
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
let data;
|
|
36
|
+
try {
|
|
37
|
+
data = JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new SensError(`invalid response from server: ${text.slice(0, 200)}`, res.status);
|
|
41
|
+
}
|
|
42
|
+
const obj = data;
|
|
43
|
+
if (!res.ok || obj.error) {
|
|
44
|
+
throw new SensError(obj.error ?? `HTTP ${res.status}`, res.status);
|
|
45
|
+
}
|
|
46
|
+
return { columns: obj.columns ?? [], rows: obj.rows ?? [] };
|
|
47
|
+
},
|
|
48
|
+
async health() {
|
|
49
|
+
const res = await doFetch(`${url}/health`);
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
throw new SensError(`HTTP ${res.status}`, res.status);
|
|
52
|
+
}
|
|
53
|
+
return (await res.json());
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dotteamdev/sensdb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency TypeScript client for a Sens (sens serve) HTTP/JSON endpoint — relational + AI-native (vector/semantic search) database.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/sens.js",
|
|
7
|
+
"module": "./dist/sens.js",
|
|
8
|
+
"types": "./dist/sens.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/sens.d.ts",
|
|
12
|
+
"import": "./dist/sens.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"sens.ts",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"sens",
|
|
26
|
+
"database",
|
|
27
|
+
"sql",
|
|
28
|
+
"vector",
|
|
29
|
+
"semantic-search",
|
|
30
|
+
"embeddings",
|
|
31
|
+
"ai",
|
|
32
|
+
"client"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.4.0"
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false
|
|
42
|
+
}
|
package/sens.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sens TypeScript client — zero dependencies, uses only `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Talks to a `sens serve` HTTP/JSON endpoint. Server-side only: keep the token
|
|
5
|
+
* on the server (Next.js route handlers / server actions, NestJS providers) and
|
|
6
|
+
* never expose it to the browser. Always pass user input as `params` — never
|
|
7
|
+
* concatenate it into the SQL string.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SensValue = number | string | boolean | null | number[];
|
|
11
|
+
|
|
12
|
+
export interface SensResult {
|
|
13
|
+
/** Column names, in order. Empty for non-SELECT statements. */
|
|
14
|
+
columns: string[];
|
|
15
|
+
/** Result rows; each row is an array of values aligned with `columns`. */
|
|
16
|
+
rows: SensValue[][];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SensError extends Error {
|
|
20
|
+
constructor(message: string, readonly status?: number) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "SensError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SensClient {
|
|
27
|
+
/** Run a (parameterized) SQL statement. `?` placeholders are filled from `params`. */
|
|
28
|
+
query(sql: string, params?: SensValue[]): Promise<SensResult>;
|
|
29
|
+
/** Server health + version. */
|
|
30
|
+
health(): Promise<{ status: string; version: string }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SensClientOptions {
|
|
34
|
+
/** Bearer token, if the server was started with --token. */
|
|
35
|
+
token?: string;
|
|
36
|
+
/** Optional custom fetch (e.g. for tests or a proxy). Defaults to global fetch. */
|
|
37
|
+
fetch?: typeof fetch;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a Sens client bound to a `sens serve` base URL, e.g.
|
|
42
|
+
* const db = createClient("http://127.0.0.1:8080", { token: process.env.SENS_TOKEN });
|
|
43
|
+
*/
|
|
44
|
+
export function createClient(baseUrl: string, opts: SensClientOptions = {}): SensClient {
|
|
45
|
+
const url = baseUrl.replace(/\/+$/, "");
|
|
46
|
+
const doFetch = opts.fetch ?? fetch;
|
|
47
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
48
|
+
if (opts.token) {
|
|
49
|
+
headers["authorization"] = `Bearer ${opts.token}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
async query(sql: string, params: SensValue[] = []): Promise<SensResult> {
|
|
54
|
+
const res = await doFetch(`${url}/query`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers,
|
|
57
|
+
body: JSON.stringify({ sql, params }),
|
|
58
|
+
});
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
let data: unknown;
|
|
61
|
+
try {
|
|
62
|
+
data = JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new SensError(`invalid response from server: ${text.slice(0, 200)}`, res.status);
|
|
65
|
+
}
|
|
66
|
+
const obj = data as { error?: string; columns?: string[]; rows?: SensValue[][] };
|
|
67
|
+
if (!res.ok || obj.error) {
|
|
68
|
+
throw new SensError(obj.error ?? `HTTP ${res.status}`, res.status);
|
|
69
|
+
}
|
|
70
|
+
return { columns: obj.columns ?? [], rows: obj.rows ?? [] };
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async health(): Promise<{ status: string; version: string }> {
|
|
74
|
+
const res = await doFetch(`${url}/health`);
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new SensError(`HTTP ${res.status}`, res.status);
|
|
77
|
+
}
|
|
78
|
+
return (await res.json()) as { status: string; version: string };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|