sqlite-hub-client 0.2.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,412 @@
1
+ # sqlite-db-client
2
+
3
+ High-level TypeScript/JavaScript client for [sqlite-db-hub](https://github.com/0xdps/sqlite-db-hub).
4
+ Comes with a full set of APIs for schema management, reads, writes — all using an **adapter** abstraction so the same code works over HTTP today and can talk to SQLite directly later.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install sqlite-db-client
10
+ ```
11
+
12
+ ## Quick start
13
+
14
+ ```ts
15
+ import { connect } from "sqlite-db-client";
16
+
17
+ const db = connect({
18
+ url: process.env.SQLITE_DB_HUB_URL, // e.g. https://my-app.up.railway.app
19
+ token: process.env.SQLITE_DB_HUB_TOKEN, // ADMIN_TOKEN set on the service
20
+ db: "my-service", // database name
21
+ });
22
+
23
+ // Create table
24
+ await db.createTable("users", [
25
+ { name: "id", type: "INTEGER", primaryKey: true, autoIncrement: true },
26
+ { name: "email", type: "TEXT", notNull: true, unique: true },
27
+ { name: "name", type: "TEXT" },
28
+ { name: "created_at", type: "TEXT", default: "(datetime('now'))" },
29
+ ]);
30
+
31
+ // Create index
32
+ await db.createIndex("idx_users_email", "users", ["email"], { unique: true });
33
+
34
+ // Insert
35
+ const { lastInsertRowid } = await db.insert("users", {
36
+ email: "alice@example.com",
37
+ name: "Alice",
38
+ });
39
+
40
+ // Bulk insert
41
+ await db.insertMany("users", [
42
+ { email: "bob@example.com", name: "Bob" },
43
+ { email: "carol@example.com", name: "Carol" },
44
+ ]);
45
+
46
+ // Read — find all
47
+ const users = await db.find<{ id: number; email: string; name: string }>(
48
+ "users"
49
+ );
50
+
51
+ // Read — filtered, paginated, ordered
52
+ const page = await db.find(
53
+ "users",
54
+ {},
55
+ {
56
+ orderBy: "created_at",
57
+ order: "DESC",
58
+ limit: 10,
59
+ offset: 0,
60
+ }
61
+ );
62
+
63
+ // Read — single row by arbitrary filter
64
+ const alice = await db.findOne("users", { email: "alice@example.com" });
65
+
66
+ // Read — by primary key
67
+ const user = await db.findById("users", 1);
68
+
69
+ // Count
70
+ const total = await db.count("users");
71
+ const active = await db.count("users", { active: 1 });
72
+
73
+ // Exists check
74
+ const exists = await db.exists("users", { email: "alice@example.com" });
75
+
76
+ // Update
77
+ await db.update("users", { name: "Alice Smith" }, { id: 1 });
78
+
79
+ // Delete
80
+ await db.delete("users", { id: 1 });
81
+
82
+ // Drop table
83
+ await db.dropTable("users");
84
+
85
+ // Raw SQL escape hatch
86
+ const result = await db.exec("PRAGMA table_info(users)");
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### `connect(options)` — create a database client
92
+
93
+ | Option | Type | Required | Description |
94
+ | --------- | -------- | -------- | ----------------------------------------------------- |
95
+ | `url` | `string` | ✅ | Base URL of your sqlite-db-hub deployment |
96
+ | `token` | `string` | ✅ | `ADMIN_TOKEN` configured on the sqlite-db-hub service |
97
+ | `db` | `string` | ✅ | Name of the database to operate on |
98
+ | `timeout` | `number` | ❌ | Request timeout in ms (default: `10000`) |
99
+
100
+ Returns a `Database` instance.
101
+
102
+ ---
103
+
104
+ ### Schema
105
+
106
+ #### `db.createTable(table, columns, options?)`
107
+
108
+ ```ts
109
+ await db.createTable(
110
+ "posts",
111
+ [
112
+ { name: "id", type: "INTEGER", primaryKey: true, autoIncrement: true },
113
+ { name: "title", type: "TEXT", notNull: true },
114
+ { name: "body", type: "TEXT" },
115
+ ],
116
+ { ifNotExists: true }
117
+ ); // ifNotExists: true is the default
118
+ ```
119
+
120
+ **`ColumnDef` fields:**
121
+
122
+ | Field | Type | Description |
123
+ | --------------- | ------------------ | ---------------------------------------------- |
124
+ | `name` | `string` | Column name |
125
+ | `type` | `string` | SQLite type: `INTEGER`, `TEXT`, `REAL`, `BLOB` |
126
+ | `primaryKey` | `boolean` | Mark as PRIMARY KEY |
127
+ | `autoIncrement` | `boolean` | Add AUTOINCREMENT |
128
+ | `notNull` | `boolean` | Add NOT NULL constraint |
129
+ | `unique` | `boolean` | Add UNIQUE constraint |
130
+ | `default` | `string \| number` | DEFAULT value (raw SQL fragment) |
131
+
132
+ #### `db.dropTable(table, ifExists?)`
133
+
134
+ ```ts
135
+ await db.dropTable("posts"); // IF EXISTS by default
136
+ ```
137
+
138
+ #### `db.createIndex(indexName, table, columns, options?)`
139
+
140
+ ```ts
141
+ await db.createIndex("idx_posts_title", "posts", ["title"]);
142
+ await db.createIndex("idx_unique_email", "users", ["email"], { unique: true });
143
+ ```
144
+
145
+ #### `db.dropIndex(indexName, ifExists?)`
146
+
147
+ ```ts
148
+ await db.dropIndex("idx_posts_title");
149
+ ```
150
+
151
+ ---
152
+
153
+ ### Write
154
+
155
+ #### `db.insert(table, data)` → `ExecResult`
156
+
157
+ ```ts
158
+ const { lastInsertRowid } = await db.insert("posts", {
159
+ title: "Hello",
160
+ body: "World",
161
+ });
162
+ ```
163
+
164
+ #### `db.insertMany(table, rows)` → `ExecResult`
165
+
166
+ ```ts
167
+ await db.insertMany("posts", [
168
+ { title: "Post 1", body: "..." },
169
+ { title: "Post 2", body: "..." },
170
+ ]);
171
+ ```
172
+
173
+ #### `db.update(table, data, where)` → `ExecResult`
174
+
175
+ ```ts
176
+ const { rowsAffected } = await db.update(
177
+ "posts",
178
+ { title: "Updated" },
179
+ { id: 1 }
180
+ );
181
+ ```
182
+
183
+ #### `db.delete(table, where)` → `ExecResult`
184
+
185
+ ```ts
186
+ await db.delete("posts", { id: 1 });
187
+ ```
188
+
189
+ ---
190
+
191
+ ### Read
192
+
193
+ #### `db.find<T>(table, where?, options?)` → `T[]`
194
+
195
+ ```ts
196
+ const posts = await db.find<Post>(
197
+ "posts",
198
+ { published: 1 },
199
+ {
200
+ columns: ["id", "title"],
201
+ orderBy: "created_at",
202
+ order: "DESC", // "ASC" | "DESC"
203
+ limit: 20,
204
+ offset: 0,
205
+ }
206
+ );
207
+ ```
208
+
209
+ #### `db.findOne<T>(table, where?)` → `T | null`
210
+
211
+ ```ts
212
+ const post = await db.findOne<Post>("posts", { id: 5 });
213
+ ```
214
+
215
+ #### `db.findById<T>(table, id, idColumn?)` → `T | null`
216
+
217
+ ```ts
218
+ const post = await db.findById<Post>("posts", 5); // uses "id" column
219
+ const item = await db.findById<Item>("items", "abc", "slug"); // custom PK column
220
+ ```
221
+
222
+ #### `db.count(table, where?)` → `number`
223
+
224
+ ```ts
225
+ const total = await db.count("posts");
226
+ const drafts = await db.count("posts", { published: 0 });
227
+ ```
228
+
229
+ #### `db.exists(table, where)` → `boolean`
230
+
231
+ ```ts
232
+ const taken = await db.exists("users", { email: "alice@example.com" });
233
+ ```
234
+
235
+ ---
236
+
237
+ ### Raw SQL
238
+
239
+ #### `db.exec<T>(sql, bindings?)` → `QueryResult<T> | ExecResult`
240
+
241
+ ```ts
242
+ // SELECT → QueryResult
243
+ const result = await db.exec<{ n: number }>("SELECT COUNT(*) AS n FROM posts");
244
+
245
+ // DDL / DML → ExecResult
246
+ await db.exec("CREATE INDEX IF NOT EXISTS idx_title ON posts (title)");
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Architecture
252
+
253
+ ```
254
+ sqlite-db-client
255
+ ├── index.ts ← connect() factory + all public exports
256
+ ├── database.ts ← Database class — all high-level APIs
257
+ └── adapters/
258
+ ├── types.ts ← IAdapter interface (exec only)
259
+ ├── http.ts ← HttpAdapter (sqlite-db-hub over HTTP)
260
+ └── index.ts ← re-exports
261
+ ```
262
+
263
+ Adding a direct SQLite adapter in the future is a one-liner:
264
+
265
+ ```ts
266
+ // future
267
+ import { Database } from "sqlite-db-client";
268
+ import { DirectAdapter } from "sqlite-db-client/adapters/direct"; // coming soon
269
+
270
+ const db = new Database(new DirectAdapter({ path: "./local.db" }));
271
+ // same API — createTable, find, insert, update, delete…
272
+ ```
273
+
274
+ ## License
275
+
276
+ MIT
277
+
278
+ ## Install
279
+
280
+ ```bash
281
+ npm install sqlite-db-hub-client
282
+ ```
283
+
284
+ Or directly from GitHub (before the npm package is published):
285
+
286
+ ```bash
287
+ npm install github:0xdps/sqlite-db-hub-client
288
+ ```
289
+
290
+ ## Quick start
291
+
292
+ ```ts
293
+ import { createClient } from "sqlite-db-hub-client";
294
+
295
+ const db = createClient({
296
+ url: process.env.SQLITE_DB_HUB_URL, // e.g. https://my-app.up.railway.app
297
+ token: process.env.SQLITE_DB_HUB_TOKEN, // ADMIN_TOKEN set on the service
298
+ db: "my-service", // name of the database to use
299
+ });
300
+
301
+ // Create a table
302
+ await db.run(`
303
+ CREATE TABLE IF NOT EXISTS users (
304
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
305
+ email TEXT NOT NULL UNIQUE,
306
+ name TEXT
307
+ )
308
+ `);
309
+
310
+ // Insert a row
311
+ await db.run("INSERT INTO users (email, name) VALUES (?, ?)", [
312
+ "alice@example.com",
313
+ "Alice",
314
+ ]);
315
+
316
+ // Query rows (typed)
317
+ const users = await db.query<{ id: number; email: string; name: string }>(
318
+ "SELECT * FROM users"
319
+ );
320
+
321
+ // Query a single row (or null)
322
+ const user = await db.queryOne<{ id: number; email: string }>(
323
+ "SELECT * FROM users WHERE email = ?",
324
+ ["alice@example.com"]
325
+ );
326
+ ```
327
+
328
+ ## API
329
+
330
+ ### `createClient(options)` / `new FileDbClient(options)`
331
+
332
+ | Option | Type | Required | Description |
333
+ | --------- | -------- | -------- | ----------------------------------------------------- |
334
+ | `url` | `string` | ✅ | Base URL of your sqlite-db-hub deployment |
335
+ | `token` | `string` | ✅ | `ADMIN_TOKEN` configured on the sqlite-db-hub service |
336
+ | `db` | `string` | ✅ | Name of the database to operate on |
337
+ | `timeout` | `number` | ❌ | Request timeout in ms (default: `10000`) |
338
+
339
+ ---
340
+
341
+ ### `db.exec(sql, bindings?)`
342
+
343
+ Run any SQL statement. Returns a `QueryResult` for SELECT, or an `ExecResult` for writes.
344
+
345
+ ```ts
346
+ const result = await db.exec("SELECT count(*) as n FROM users");
347
+ // { headers: [...], rows: [{ n: 1 }], rowsRead: 1 }
348
+ ```
349
+
350
+ ---
351
+
352
+ ### `db.query<T>(sql, bindings?)`
353
+
354
+ Run a SELECT and return typed rows.
355
+
356
+ ```ts
357
+ const rows = await db.query<{ id: number; name: string }>(
358
+ "SELECT id, name FROM users WHERE id > ?",
359
+ [5]
360
+ );
361
+ ```
362
+
363
+ ---
364
+
365
+ ### `db.queryOne<T>(sql, bindings?)`
366
+
367
+ Run a SELECT and return the first row, or `null` if no results.
368
+
369
+ ```ts
370
+ const row = await db.queryOne<{ name: string }>(
371
+ "SELECT name FROM users WHERE id = ?",
372
+ [1]
373
+ );
374
+ ```
375
+
376
+ ---
377
+
378
+ ### `db.run(sql, bindings?)`
379
+
380
+ Run a write statement (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP). Returns `{ rowsAffected, lastInsertRowid }`.
381
+
382
+ ```ts
383
+ const { rowsAffected, lastInsertRowid } = await db.run(
384
+ "INSERT INTO jobs (payload) VALUES (?)",
385
+ [JSON.stringify({ task: "send-email" })]
386
+ );
387
+ ```
388
+
389
+ ## Types
390
+
391
+ ```ts
392
+ interface QueryResult<T> {
393
+ headers: ColumnHeader[];
394
+ rows: T[];
395
+ rowsRead: number;
396
+ }
397
+
398
+ interface ExecResult {
399
+ rowsAffected: number;
400
+ lastInsertRowid: number | null;
401
+ }
402
+
403
+ interface ColumnHeader {
404
+ name: string;
405
+ displayName: string;
406
+ originalType: string | null;
407
+ }
408
+ ```
409
+
410
+ ## License
411
+
412
+ MIT
@@ -0,0 +1,22 @@
1
+ import type { IAdapter, RawResult } from "./types.js";
2
+ export interface HttpAdapterOptions {
3
+ /** Base URL of the sqlite-db-hub deployment, e.g. https://my-app.up.railway.app */
4
+ url: string;
5
+ /** ADMIN_TOKEN configured on the sqlite-db-hub service */
6
+ token: string;
7
+ /** Name of the database to operate on */
8
+ db: string;
9
+ /** Request timeout in ms (default: 10 000) */
10
+ timeout?: number;
11
+ }
12
+ /**
13
+ * Adapter that executes SQL via the sqlite-db-hub HTTP API
14
+ * (POST /api/db/:name/exec).
15
+ */
16
+ export declare class HttpAdapter implements IAdapter {
17
+ private readonly options;
18
+ private readonly http;
19
+ private readonly dbPath;
20
+ constructor(options: HttpAdapterOptions);
21
+ exec<T = Record<string, unknown>>(sql: string, bindings?: unknown[]): Promise<RawResult<T>>;
22
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpAdapter = void 0;
7
+ const fetch_1 = __importDefault(require("@pingpong-js/fetch"));
8
+ /**
9
+ * Adapter that executes SQL via the sqlite-db-hub HTTP API
10
+ * (POST /api/db/:name/exec).
11
+ */
12
+ class HttpAdapter {
13
+ constructor(options) {
14
+ this.options = options;
15
+ this.dbPath = `/api/db/${encodeURIComponent(options.db)}/exec`;
16
+ this.http = fetch_1.default.create({
17
+ baseURL: options.url.replace(/\/$/, ""),
18
+ timeout: options.timeout ?? 10000,
19
+ headers: {
20
+ Authorization: `Bearer ${options.token}`,
21
+ "Content-Type": "application/json",
22
+ },
23
+ });
24
+ }
25
+ async exec(sql, bindings) {
26
+ const res = await this.http.post(this.dbPath, {
27
+ sql,
28
+ ...(bindings?.length ? { bindings } : {}),
29
+ });
30
+ if (res.isError()) {
31
+ const body = res.data;
32
+ throw new Error(body?.error ??
33
+ `sqlite-db-hub: HTTP ${res.status} for db "${this.options.db}"`);
34
+ }
35
+ return res.data;
36
+ }
37
+ }
38
+ exports.HttpAdapter = HttpAdapter;
@@ -0,0 +1,3 @@
1
+ export { HttpAdapter } from "./http.js";
2
+ export type { HttpAdapterOptions } from "./http.js";
3
+ export type { IAdapter, ColumnHeader, QueryResult, ExecResult, RawResult } from "./types.js";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpAdapter = void 0;
4
+ var http_js_1 = require("./http.js");
5
+ Object.defineProperty(exports, "HttpAdapter", { enumerable: true, get: function () { return http_js_1.HttpAdapter; } });
@@ -0,0 +1,29 @@
1
+ /** A single column header returned by a SELECT */
2
+ export interface ColumnHeader {
3
+ name: string;
4
+ displayName: string;
5
+ originalType: string | null;
6
+ }
7
+ /** Result of a SELECT statement */
8
+ export interface QueryResult<T = Record<string, unknown>> {
9
+ headers: ColumnHeader[];
10
+ rows: T[];
11
+ rowsRead: number;
12
+ }
13
+ /** Result of INSERT / UPDATE / DELETE / DDL */
14
+ export interface ExecResult {
15
+ rowsAffected: number;
16
+ lastInsertRowid: number | null;
17
+ }
18
+ export type RawResult<T = Record<string, unknown>> = QueryResult<T> | ExecResult;
19
+ /**
20
+ * Minimal contract every adapter must fulfil.
21
+ * Only `exec` is required — all high-level APIs are built on top of it.
22
+ */
23
+ export interface IAdapter {
24
+ /**
25
+ * Execute a SQL statement with optional positional bindings.
26
+ * Returns QueryResult for SELECT, ExecResult for everything else.
27
+ */
28
+ exec<T = Record<string, unknown>>(sql: string, bindings?: unknown[]): Promise<RawResult<T>>;
29
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,141 @@
1
+ import type { IAdapter, ExecResult, RawResult } from "./adapters/types.js";
2
+ export type WhereClause = Record<string, unknown>;
3
+ export type OrderDirection = "ASC" | "DESC";
4
+ export interface FindOptions {
5
+ /** Columns to SELECT (default: all) */
6
+ columns?: string[];
7
+ orderBy?: string;
8
+ order?: OrderDirection;
9
+ limit?: number;
10
+ offset?: number;
11
+ }
12
+ export interface ColumnDef {
13
+ name: string;
14
+ type: "INTEGER" | "TEXT" | "REAL" | "BLOB" | "NUMERIC" | string;
15
+ primaryKey?: boolean;
16
+ autoIncrement?: boolean;
17
+ notNull?: boolean;
18
+ unique?: boolean;
19
+ default?: string | number;
20
+ }
21
+ export interface CreateTableOptions {
22
+ ifNotExists?: boolean;
23
+ }
24
+ export interface CreateIndexOptions {
25
+ unique?: boolean;
26
+ ifNotExists?: boolean;
27
+ }
28
+ /**
29
+ * High-level database client.
30
+ * Accepts any `IAdapter` — currently `HttpAdapter`, extensible to direct
31
+ * SQLite (via better-sqlite3 or sql.js) without changing business code.
32
+ */
33
+ export declare class Database {
34
+ private readonly adapter;
35
+ constructor(adapter: IAdapter);
36
+ /**
37
+ * Create a table.
38
+ *
39
+ * @example
40
+ * await db.createTable("users", [
41
+ * { name: "id", type: "INTEGER", primaryKey: true, autoIncrement: true },
42
+ * { name: "email", type: "TEXT", notNull: true, unique: true },
43
+ * { name: "name", type: "TEXT" },
44
+ * ]);
45
+ */
46
+ createTable(table: string, columns: ColumnDef[], options?: CreateTableOptions): Promise<ExecResult>;
47
+ /**
48
+ * Drop a table.
49
+ */
50
+ dropTable(table: string, ifExists?: boolean): Promise<ExecResult>;
51
+ /**
52
+ * Create an index on one or more columns.
53
+ *
54
+ * @example
55
+ * await db.createIndex("idx_users_email", "users", ["email"], { unique: true });
56
+ */
57
+ createIndex(indexName: string, table: string, columns: string[], options?: CreateIndexOptions): Promise<ExecResult>;
58
+ /**
59
+ * Drop an index.
60
+ */
61
+ dropIndex(indexName: string, ifExists?: boolean): Promise<ExecResult>;
62
+ /**
63
+ * Insert a single row. Returns `{ rowsAffected, lastInsertRowid }`.
64
+ *
65
+ * @example
66
+ * const { lastInsertRowid } = await db.insert("users", { email: "a@b.com", name: "Alice" });
67
+ */
68
+ insert(table: string, data: Record<string, unknown>): Promise<ExecResult>;
69
+ /**
70
+ * Insert multiple rows in a single transaction.
71
+ *
72
+ * @example
73
+ * await db.insertMany("users", [
74
+ * { email: "a@b.com", name: "Alice" },
75
+ * { email: "b@c.com", name: "Bob" },
76
+ * ]);
77
+ */
78
+ insertMany(table: string, rows: Record<string, unknown>[]): Promise<ExecResult>;
79
+ /**
80
+ * Update rows matching `where`.
81
+ *
82
+ * @example
83
+ * await db.update("users", { name: "Bob" }, { id: 1 });
84
+ */
85
+ update(table: string, data: Record<string, unknown>, where: WhereClause): Promise<ExecResult>;
86
+ /**
87
+ * Delete rows matching `where`.
88
+ *
89
+ * @example
90
+ * await db.delete("users", { id: 1 });
91
+ */
92
+ delete(table: string, where: WhereClause): Promise<ExecResult>;
93
+ /**
94
+ * Find rows with optional filtering, ordering, and pagination.
95
+ *
96
+ * @example
97
+ * const users = await db.find<User>("users", { active: 1 }, {
98
+ * columns: ["id", "email"],
99
+ * orderBy: "created_at",
100
+ * order: "DESC",
101
+ * limit: 20,
102
+ * offset: 0,
103
+ * });
104
+ */
105
+ find<T = Record<string, unknown>>(table: string, where?: WhereClause, options?: FindOptions): Promise<T[]>;
106
+ /**
107
+ * Find the first row matching `where`, or `null`.
108
+ *
109
+ * @example
110
+ * const user = await db.findOne<User>("users", { email: "a@b.com" });
111
+ */
112
+ findOne<T = Record<string, unknown>>(table: string, where?: WhereClause): Promise<T | null>;
113
+ /**
114
+ * Find a row by its primary key value (column `id` by default).
115
+ *
116
+ * @example
117
+ * const user = await db.findById<User>("users", 42);
118
+ */
119
+ findById<T = Record<string, unknown>>(table: string, id: unknown, idColumn?: string): Promise<T | null>;
120
+ /**
121
+ * Count rows matching `where`.
122
+ *
123
+ * @example
124
+ * const total = await db.count("users");
125
+ * const active = await db.count("users", { active: 1 });
126
+ */
127
+ count(table: string, where?: WhereClause): Promise<number>;
128
+ /**
129
+ * Check whether any row matching `where` exists.
130
+ */
131
+ exists(table: string, where: WhereClause): Promise<boolean>;
132
+ /**
133
+ * Run any raw SQL statement.
134
+ *
135
+ * @example
136
+ * const result = await db.exec("PRAGMA table_info(users)");
137
+ */
138
+ exec<T = Record<string, unknown>>(sql: string, bindings?: unknown[]): Promise<RawResult<T>>;
139
+ private _read;
140
+ private _write;
141
+ }
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Database = void 0;
4
+ // ── SQL builder helpers ─────────────────────────────────────────────────────
5
+ function buildWhere(where) {
6
+ const keys = Object.keys(where);
7
+ if (keys.length === 0)
8
+ return { clause: "", bindings: [] };
9
+ const parts = keys.map((k) => `"${k}" = ?`);
10
+ return {
11
+ clause: " WHERE " + parts.join(" AND "),
12
+ bindings: keys.map((k) => where[k]),
13
+ };
14
+ }
15
+ function isQueryResult(r) {
16
+ return "rows" in r;
17
+ }
18
+ function isExecResult(r) {
19
+ return "rowsAffected" in r;
20
+ }
21
+ // ── Database ─────────────────────────────────────────────────────────────────
22
+ /**
23
+ * High-level database client.
24
+ * Accepts any `IAdapter` — currently `HttpAdapter`, extensible to direct
25
+ * SQLite (via better-sqlite3 or sql.js) without changing business code.
26
+ */
27
+ class Database {
28
+ constructor(adapter) {
29
+ this.adapter = adapter;
30
+ }
31
+ // ── Schema ──────────────────────────────────────────────────────────────
32
+ /**
33
+ * Create a table.
34
+ *
35
+ * @example
36
+ * await db.createTable("users", [
37
+ * { name: "id", type: "INTEGER", primaryKey: true, autoIncrement: true },
38
+ * { name: "email", type: "TEXT", notNull: true, unique: true },
39
+ * { name: "name", type: "TEXT" },
40
+ * ]);
41
+ */
42
+ async createTable(table, columns, options = {}) {
43
+ const ifNotExists = options.ifNotExists !== false ? "IF NOT EXISTS" : "";
44
+ const cols = columns.map((c) => {
45
+ let def = `"${c.name}" ${c.type}`;
46
+ if (c.primaryKey)
47
+ def += " PRIMARY KEY";
48
+ if (c.autoIncrement)
49
+ def += " AUTOINCREMENT";
50
+ if (c.notNull)
51
+ def += " NOT NULL";
52
+ if (c.unique)
53
+ def += " UNIQUE";
54
+ if (c.default !== undefined)
55
+ def += ` DEFAULT ${c.default}`;
56
+ return def;
57
+ });
58
+ const sql = `CREATE TABLE ${ifNotExists} "${table}" (${cols.join(", ")})`;
59
+ return this._write(sql);
60
+ }
61
+ /**
62
+ * Drop a table.
63
+ */
64
+ async dropTable(table, ifExists = true) {
65
+ const ie = ifExists ? "IF EXISTS" : "";
66
+ return this._write(`DROP TABLE ${ie} "${table}"`);
67
+ }
68
+ /**
69
+ * Create an index on one or more columns.
70
+ *
71
+ * @example
72
+ * await db.createIndex("idx_users_email", "users", ["email"], { unique: true });
73
+ */
74
+ async createIndex(indexName, table, columns, options = {}) {
75
+ const unique = options.unique ? "UNIQUE" : "";
76
+ const ifNotExists = options.ifNotExists !== false ? "IF NOT EXISTS" : "";
77
+ const cols = columns.map((c) => `"${c}"`).join(", ");
78
+ const sql = `CREATE ${unique} INDEX ${ifNotExists} "${indexName}" ON "${table}" (${cols})`;
79
+ return this._write(sql);
80
+ }
81
+ /**
82
+ * Drop an index.
83
+ */
84
+ async dropIndex(indexName, ifExists = true) {
85
+ const ie = ifExists ? "IF EXISTS" : "";
86
+ return this._write(`DROP INDEX ${ie} "${indexName}"`);
87
+ }
88
+ // ── Write ────────────────────────────────────────────────────────────────
89
+ /**
90
+ * Insert a single row. Returns `{ rowsAffected, lastInsertRowid }`.
91
+ *
92
+ * @example
93
+ * const { lastInsertRowid } = await db.insert("users", { email: "a@b.com", name: "Alice" });
94
+ */
95
+ async insert(table, data) {
96
+ const keys = Object.keys(data);
97
+ const cols = keys.map((k) => `"${k}"`).join(", ");
98
+ const placeholders = keys.map(() => "?").join(", ");
99
+ const bindings = keys.map((k) => data[k]);
100
+ return this._write(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`, bindings);
101
+ }
102
+ /**
103
+ * Insert multiple rows in a single transaction.
104
+ *
105
+ * @example
106
+ * await db.insertMany("users", [
107
+ * { email: "a@b.com", name: "Alice" },
108
+ * { email: "b@c.com", name: "Bob" },
109
+ * ]);
110
+ */
111
+ async insertMany(table, rows) {
112
+ if (rows.length === 0)
113
+ return { rowsAffected: 0, lastInsertRowid: null };
114
+ const keys = Object.keys(rows[0]);
115
+ const cols = keys.map((k) => `"${k}"`).join(", ");
116
+ const rowPlaceholders = rows.map(() => `(${keys.map(() => "?").join(", ")})`).join(", ");
117
+ const bindings = rows.flatMap((row) => keys.map((k) => row[k]));
118
+ return this._write(`INSERT INTO "${table}" (${cols}) VALUES ${rowPlaceholders}`, bindings);
119
+ }
120
+ /**
121
+ * Update rows matching `where`.
122
+ *
123
+ * @example
124
+ * await db.update("users", { name: "Bob" }, { id: 1 });
125
+ */
126
+ async update(table, data, where) {
127
+ const setKeys = Object.keys(data);
128
+ const setClauses = setKeys.map((k) => `"${k}" = ?`).join(", ");
129
+ const setBindings = setKeys.map((k) => data[k]);
130
+ const { clause, bindings: whereBindings } = buildWhere(where);
131
+ return this._write(`UPDATE "${table}" SET ${setClauses}${clause}`, [...setBindings, ...whereBindings]);
132
+ }
133
+ /**
134
+ * Delete rows matching `where`.
135
+ *
136
+ * @example
137
+ * await db.delete("users", { id: 1 });
138
+ */
139
+ async delete(table, where) {
140
+ const { clause, bindings } = buildWhere(where);
141
+ return this._write(`DELETE FROM "${table}"${clause}`, bindings);
142
+ }
143
+ // ── Read ─────────────────────────────────────────────────────────────────
144
+ /**
145
+ * Find rows with optional filtering, ordering, and pagination.
146
+ *
147
+ * @example
148
+ * const users = await db.find<User>("users", { active: 1 }, {
149
+ * columns: ["id", "email"],
150
+ * orderBy: "created_at",
151
+ * order: "DESC",
152
+ * limit: 20,
153
+ * offset: 0,
154
+ * });
155
+ */
156
+ async find(table, where = {}, options = {}) {
157
+ const cols = options.columns?.map((c) => `"${c}"`).join(", ") ?? "*";
158
+ const { clause, bindings } = buildWhere(where);
159
+ let sql = `SELECT ${cols} FROM "${table}"${clause}`;
160
+ if (options.orderBy)
161
+ sql += ` ORDER BY "${options.orderBy}" ${options.order ?? "ASC"}`;
162
+ if (options.limit !== undefined)
163
+ sql += ` LIMIT ${options.limit}`;
164
+ if (options.offset !== undefined)
165
+ sql += ` OFFSET ${options.offset}`;
166
+ return this._read(sql, bindings);
167
+ }
168
+ /**
169
+ * Find the first row matching `where`, or `null`.
170
+ *
171
+ * @example
172
+ * const user = await db.findOne<User>("users", { email: "a@b.com" });
173
+ */
174
+ async findOne(table, where = {}) {
175
+ const rows = await this.find(table, where, { limit: 1 });
176
+ return rows[0] ?? null;
177
+ }
178
+ /**
179
+ * Find a row by its primary key value (column `id` by default).
180
+ *
181
+ * @example
182
+ * const user = await db.findById<User>("users", 42);
183
+ */
184
+ async findById(table, id, idColumn = "id") {
185
+ return this.findOne(table, { [idColumn]: id });
186
+ }
187
+ /**
188
+ * Count rows matching `where`.
189
+ *
190
+ * @example
191
+ * const total = await db.count("users");
192
+ * const active = await db.count("users", { active: 1 });
193
+ */
194
+ async count(table, where = {}) {
195
+ const { clause, bindings } = buildWhere(where);
196
+ const sql = `SELECT COUNT(*) AS n FROM "${table}"${clause}`;
197
+ const rows = await this._read(sql, bindings);
198
+ return rows[0]?.n ?? 0;
199
+ }
200
+ /**
201
+ * Check whether any row matching `where` exists.
202
+ */
203
+ async exists(table, where) {
204
+ return (await this.count(table, where)) > 0;
205
+ }
206
+ // ── Raw escape hatch ─────────────────────────────────────────────────────
207
+ /**
208
+ * Run any raw SQL statement.
209
+ *
210
+ * @example
211
+ * const result = await db.exec("PRAGMA table_info(users)");
212
+ */
213
+ async exec(sql, bindings) {
214
+ return this.adapter.exec(sql, bindings);
215
+ }
216
+ // ── Internal ─────────────────────────────────────────────────────────────
217
+ async _read(sql, bindings) {
218
+ const result = await this.adapter.exec(sql, bindings);
219
+ if (!isQueryResult(result)) {
220
+ throw new Error(`sqlite-db-hub: expected SELECT, got write result for: ${sql}`);
221
+ }
222
+ return result.rows;
223
+ }
224
+ async _write(sql, bindings) {
225
+ const result = await this.adapter.exec(sql, bindings);
226
+ if (!isExecResult(result)) {
227
+ throw new Error(`sqlite-db-hub: expected write result, got SELECT for: ${sql}`);
228
+ }
229
+ return result;
230
+ }
231
+ }
232
+ exports.Database = Database;
@@ -0,0 +1,19 @@
1
+ export { Database } from "./database.js";
2
+ export type { WhereClause, FindOptions, ColumnDef, CreateTableOptions, CreateIndexOptions, OrderDirection, } from "./database.js";
3
+ export { HttpAdapter } from "./adapters/http.js";
4
+ export type { HttpAdapterOptions } from "./adapters/http.js";
5
+ export type { IAdapter, ColumnHeader, QueryResult, ExecResult, RawResult, } from "./adapters/types.js";
6
+ import { Database } from "./database.js";
7
+ import { type HttpAdapterOptions } from "./adapters/http.js";
8
+ /**
9
+ * Create a Database connected to a sqlite-db-hub service over HTTP.
10
+ *
11
+ * @example
12
+ * const db = connect({
13
+ * url: process.env.SQLITE_DB_HUB_URL,
14
+ * token: process.env.SQLITE_DB_HUB_TOKEN,
15
+ * db: "my-service",
16
+ * });
17
+ */
18
+ export declare function connect(options: HttpAdapterOptions): Database;
19
+ export default connect;
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpAdapter = exports.Database = void 0;
4
+ exports.connect = connect;
5
+ var database_js_1 = require("./database.js");
6
+ Object.defineProperty(exports, "Database", { enumerable: true, get: function () { return database_js_1.Database; } });
7
+ var http_js_1 = require("./adapters/http.js");
8
+ Object.defineProperty(exports, "HttpAdapter", { enumerable: true, get: function () { return http_js_1.HttpAdapter; } });
9
+ // ── Convenience factory ──────────────────────────────────────────────────────
10
+ const database_js_2 = require("./database.js");
11
+ const http_js_2 = require("./adapters/http.js");
12
+ /**
13
+ * Create a Database connected to a sqlite-db-hub service over HTTP.
14
+ *
15
+ * @example
16
+ * const db = connect({
17
+ * url: process.env.SQLITE_DB_HUB_URL,
18
+ * token: process.env.SQLITE_DB_HUB_TOKEN,
19
+ * db: "my-service",
20
+ * });
21
+ */
22
+ function connect(options) {
23
+ return new database_js_2.Database(new http_js_2.HttpAdapter(options));
24
+ }
25
+ exports.default = connect;
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "sqlite-hub-client",
3
+ "version": "0.2.0",
4
+ "description": "High-level SQLite client for sqlite-db-hub — HTTP adapter included, direct SQLite coming soon",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist", "README.md"],
8
+ "scripts": {
9
+ "build": "tsc"
10
+ },
11
+ "keywords": ["sqlite", "sqlite-db-hub", "railway", "orm", "database"],
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "@pingpong-js/fetch": "^1.0.2"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5"
18
+ }
19
+ }