@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 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
+ }