basaltdb 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/LICENSE +21 -0
- package/README.md +116 -0
- package/package.json +38 -0
- package/src/engine.js +16 -0
- package/src/index.d.ts +68 -0
- package/src/index.js +160 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Type definitions for basaltdb
|
|
2
|
+
|
|
3
|
+
export class BasaltError extends Error {}
|
|
4
|
+
|
|
5
|
+
export interface ColumnInfo { name: string; type: string; pk: boolean; indexed: boolean; }
|
|
6
|
+
export interface TableInfo { name: string; nrows: number; columns: ColumnInfo[]; }
|
|
7
|
+
export interface QueryStats {
|
|
8
|
+
scanned: number; matched: number; used_pk_index: boolean; access: string; ms: number;
|
|
9
|
+
}
|
|
10
|
+
export interface Result {
|
|
11
|
+
columns: string[];
|
|
12
|
+
types: string[];
|
|
13
|
+
rows: Array<Array<string | number | boolean | null>>;
|
|
14
|
+
total_rows: number;
|
|
15
|
+
truncated: boolean;
|
|
16
|
+
stats: QueryStats;
|
|
17
|
+
plan: string[];
|
|
18
|
+
message?: string;
|
|
19
|
+
/** Rows as `{ column: value }` objects. */
|
|
20
|
+
toObjects(): Array<Record<string, string | number | boolean | null>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Value = string | number | boolean | Date | null | undefined;
|
|
24
|
+
|
|
25
|
+
export class QueryBuilder {
|
|
26
|
+
select(...cols: string[]): this;
|
|
27
|
+
where(col: string, op: string, val: Value): this;
|
|
28
|
+
where(col: string, val: Value): this;
|
|
29
|
+
orderBy(col: string, dir?: 'ASC' | 'DESC' | 'asc' | 'desc'): this;
|
|
30
|
+
limit(n: number): this;
|
|
31
|
+
toSQL(): string;
|
|
32
|
+
all(): Promise<Record<string, any>[]> | Record<string, any>[];
|
|
33
|
+
first(): Promise<Record<string, any> | null> | (Record<string, any> | null);
|
|
34
|
+
insert(rows: Record<string, Value> | Record<string, Value>[]): Promise<Result> | Result;
|
|
35
|
+
update(patch: Record<string, Value>): Promise<Result> | Result;
|
|
36
|
+
delete(): Promise<Result> | Result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Embedded, in-process (WebAssembly) database. */
|
|
40
|
+
export class Basalt {
|
|
41
|
+
static create(): Promise<Basalt>;
|
|
42
|
+
exec(sql: string): Result;
|
|
43
|
+
query(sql: string): Record<string, any>[];
|
|
44
|
+
schema(): TableInfo[];
|
|
45
|
+
table(name: string): QueryBuilder;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BasaltClientOptions {
|
|
49
|
+
db?: string;
|
|
50
|
+
fetch?: typeof fetch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Client for a running `basalt` server (HTTP). */
|
|
54
|
+
export class BasaltClient {
|
|
55
|
+
constructor(baseUrl?: string, opts?: BasaltClientOptions);
|
|
56
|
+
db: string;
|
|
57
|
+
exec(sql: string, db?: string): Promise<Result>;
|
|
58
|
+
query(sql: string, db?: string): Promise<Record<string, any>[]>;
|
|
59
|
+
databases(): Promise<string[]>;
|
|
60
|
+
schema(db?: string): Promise<TableInfo[]>;
|
|
61
|
+
createDatabase(name: string): Promise<any>;
|
|
62
|
+
dropDatabase(name: string): Promise<any>;
|
|
63
|
+
use(db: string): this;
|
|
64
|
+
table(name: string): QueryBuilder;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function lit(v: Value): string;
|
|
68
|
+
export default Basalt;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// basaltdb — official JavaScript/TypeScript client for the Basalt database.
|
|
2
|
+
//
|
|
3
|
+
// Two ways to use it:
|
|
4
|
+
// 1. Embedded (in-process, WebAssembly) — like sql.js. Runs in Node and the
|
|
5
|
+
// browser, no server. The database lives in memory for the process lifetime.
|
|
6
|
+
// import { Basalt } from 'basaltdb';
|
|
7
|
+
// const db = await Basalt.create();
|
|
8
|
+
// db.exec("CREATE TABLE t (id BIGINT PRIMARY KEY, v INT)");
|
|
9
|
+
// 2. Client/server — talk to a running `basalt` server over HTTP (persistent).
|
|
10
|
+
// import { BasaltClient } from 'basaltdb';
|
|
11
|
+
// const db = new BasaltClient('http://127.0.0.1:8090', { db: 'mydb' });
|
|
12
|
+
//
|
|
13
|
+
// Both expose the same query surface (exec / query / a small query builder), so
|
|
14
|
+
// code can move between embedded and server with minimal changes.
|
|
15
|
+
import createBasalt from './engine.js';
|
|
16
|
+
|
|
17
|
+
export class BasaltError extends Error {
|
|
18
|
+
constructor(message) { super(message); this.name = 'BasaltError'; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Attach convenience accessors to a raw result object from the engine/server.
|
|
22
|
+
// Raw shape: { columns, types, rows, total_rows, truncated, stats, plan, message?, error? }
|
|
23
|
+
function shape(r) {
|
|
24
|
+
if (r && r.error) throw new BasaltError(r.error);
|
|
25
|
+
Object.defineProperty(r, 'toObjects', {
|
|
26
|
+
enumerable: false,
|
|
27
|
+
value() {
|
|
28
|
+
const cols = r.columns || [];
|
|
29
|
+
return (r.rows || []).map(row => {
|
|
30
|
+
const o = {};
|
|
31
|
+
for (let i = 0; i < cols.length; i++) o[cols[i]] = row[i];
|
|
32
|
+
return o;
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return r;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Await-through: works whether `v` is a value (embedded) or a Promise (HTTP).
|
|
40
|
+
function then(v, f) { return v && typeof v.then === 'function' ? v.then(f) : f(v); }
|
|
41
|
+
|
|
42
|
+
// SQL literal formatting (values only — identifiers are never taken from input).
|
|
43
|
+
export function lit(v) {
|
|
44
|
+
if (v === null || v === undefined) return 'NULL';
|
|
45
|
+
if (typeof v === 'number') return Number.isFinite(v) ? String(v) : 'NULL';
|
|
46
|
+
if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
|
|
47
|
+
if (v instanceof Date) return `'${v.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
|
48
|
+
return `'${String(v).replace(/'/g, "''")}'`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- fluent query builder (ORM-lite) -------------------------------------
|
|
52
|
+
// Generates SQL and runs it through whichever runner created it. Not a full ORM
|
|
53
|
+
// (see the README for what Basalt's SQL surface does and doesn't support yet).
|
|
54
|
+
class QueryBuilder {
|
|
55
|
+
constructor(runner, table) {
|
|
56
|
+
this._r = runner; this._t = table;
|
|
57
|
+
this._sel = '*'; this._where = []; this._order = null; this._limit = null;
|
|
58
|
+
}
|
|
59
|
+
select(...cols) { this._sel = cols.length ? cols.join(', ') : '*'; return this; }
|
|
60
|
+
where(col, op, val) {
|
|
61
|
+
if (val === undefined) { val = op; op = '='; } // .where('id', 1) → id = 1
|
|
62
|
+
this._where.push(`${col} ${op} ${lit(val)}`); return this;
|
|
63
|
+
}
|
|
64
|
+
orderBy(col, dir = 'ASC') { this._order = `${col} ${String(dir).toUpperCase()}`; return this; }
|
|
65
|
+
limit(n) { this._limit = n | 0; return this; }
|
|
66
|
+
|
|
67
|
+
toSQL() {
|
|
68
|
+
let s = `SELECT ${this._sel} FROM ${this._t}`;
|
|
69
|
+
if (this._where.length) s += ` WHERE ${this._where.join(' AND ')}`;
|
|
70
|
+
if (this._order) s += ` ORDER BY ${this._order}`;
|
|
71
|
+
if (this._limit != null) s += ` LIMIT ${this._limit}`;
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
/** Run the SELECT and return an array of row objects. */
|
|
75
|
+
all() { return then(this._r._run(this.toSQL()), r => r.toObjects()); }
|
|
76
|
+
/** Run the SELECT and return the first row object (or null). */
|
|
77
|
+
first() { this.limit(1); return then(this.all(), rows => rows[0] ?? null); }
|
|
78
|
+
|
|
79
|
+
/** INSERT one object or an array of objects. */
|
|
80
|
+
insert(rows) {
|
|
81
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
82
|
+
if (!list.length) throw new BasaltError('insert(): no rows');
|
|
83
|
+
const cols = Object.keys(list[0]);
|
|
84
|
+
const vals = list.map(o => `(${cols.map(c => lit(o[c])).join(', ')})`).join(', ');
|
|
85
|
+
return this._r._run(`INSERT INTO ${this._t} (${cols.join(', ')}) VALUES ${vals}`);
|
|
86
|
+
}
|
|
87
|
+
/** UPDATE with the accumulated WHERE. */
|
|
88
|
+
update(patch) {
|
|
89
|
+
const set = Object.keys(patch).map(c => `${c} = ${lit(patch[c])}`).join(', ');
|
|
90
|
+
let s = `UPDATE ${this._t} SET ${set}`;
|
|
91
|
+
if (this._where.length) s += ` WHERE ${this._where.join(' AND ')}`;
|
|
92
|
+
return this._r._run(s);
|
|
93
|
+
}
|
|
94
|
+
/** DELETE with the accumulated WHERE. */
|
|
95
|
+
delete() {
|
|
96
|
+
let s = `DELETE FROM ${this._t}`;
|
|
97
|
+
if (this._where.length) s += ` WHERE ${this._where.join(' AND ')}`;
|
|
98
|
+
return this._r._run(s);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- embedded engine (WASM, in-process) ----------------------------------
|
|
103
|
+
export class Basalt {
|
|
104
|
+
constructor(mod) {
|
|
105
|
+
this._m = mod;
|
|
106
|
+
this._exec = mod.cwrap('hydb_exec', 'string', ['string']);
|
|
107
|
+
this._schemaFn = mod.cwrap('hydb_schema', 'string', []);
|
|
108
|
+
}
|
|
109
|
+
/** Create an in-memory Basalt database (seeded with a small demo dataset). */
|
|
110
|
+
static async create() {
|
|
111
|
+
const mod = await createBasalt();
|
|
112
|
+
mod.ccall('hydb_init');
|
|
113
|
+
return new Basalt(mod);
|
|
114
|
+
}
|
|
115
|
+
/** Execute one SQL statement; returns the full result object (throws BasaltError on error). */
|
|
116
|
+
exec(sql) { return shape(JSON.parse(this._exec(String(sql)))); }
|
|
117
|
+
_run(sql) { return this.exec(sql); }
|
|
118
|
+
/** Execute a SELECT and return an array of row objects. */
|
|
119
|
+
query(sql) { return this.exec(sql).toObjects(); }
|
|
120
|
+
/** Current schema: [{ name, nrows, columns:[{name,type,pk,indexed}] }]. */
|
|
121
|
+
schema() { return JSON.parse(this._schemaFn()).tables; }
|
|
122
|
+
/** Start a query builder on a table. */
|
|
123
|
+
table(name) { return new QueryBuilder(this, name); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- HTTP client (talks to a running `basalt` server) --------------------
|
|
127
|
+
export class BasaltClient {
|
|
128
|
+
constructor(baseUrl = 'http://127.0.0.1:8090', opts = {}) {
|
|
129
|
+
this.base = String(baseUrl).replace(/\/+$/, '');
|
|
130
|
+
this.db = opts.db || '';
|
|
131
|
+
this._fetch = opts.fetch || globalThis.fetch;
|
|
132
|
+
if (!this._fetch) throw new BasaltError('no fetch available; pass opts.fetch');
|
|
133
|
+
}
|
|
134
|
+
async _post(path, body) {
|
|
135
|
+
const res = await this._fetch(this.base + path, { method: 'POST', body: body ?? '' });
|
|
136
|
+
return res.json();
|
|
137
|
+
}
|
|
138
|
+
async _get(path) { const res = await this._fetch(this.base + path); return res.json(); }
|
|
139
|
+
|
|
140
|
+
/** Execute one SQL statement against `db` (defaults to the client's db). */
|
|
141
|
+
async exec(sql, db = this.db) {
|
|
142
|
+
return shape(await this._post(`/api/query?db=${encodeURIComponent(db)}`, String(sql)));
|
|
143
|
+
}
|
|
144
|
+
_run(sql) { return this.exec(sql); }
|
|
145
|
+
async query(sql, db) { return (await this.exec(sql, db)).toObjects(); }
|
|
146
|
+
|
|
147
|
+
/** List database directories the server is serving. */
|
|
148
|
+
databases() { return this._get('/api/databases'); }
|
|
149
|
+
/** Schema for a database. */
|
|
150
|
+
async schema(db = this.db) { return (await this._get(`/api/schema?db=${encodeURIComponent(db)}`)).tables; }
|
|
151
|
+
createDatabase(name) { return this._post(`/api/create_db?db=${encodeURIComponent(name)}`); }
|
|
152
|
+
dropDatabase(name) { return this._post(`/api/drop_db?db=${encodeURIComponent(name)}`); }
|
|
153
|
+
|
|
154
|
+
/** Use a specific database for subsequent calls (returns this). */
|
|
155
|
+
use(db) { this.db = db; return this; }
|
|
156
|
+
/** Start a query builder on a table. */
|
|
157
|
+
table(name) { return new QueryBuilder(this, name); }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default Basalt;
|