stitchdb 1.1.0 → 1.3.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/dist/index.d.mts CHANGED
@@ -32,6 +32,12 @@ declare class StitchDBError extends Error {
32
32
  status: number;
33
33
  constructor(message: string, status?: number);
34
34
  }
35
+ /**
36
+ * StitchDB client.
37
+ *
38
+ * Uses WebSocket for queries (1 connection, unlimited queries, $0.019/M cost).
39
+ * Falls back to HTTP if WebSocket unavailable.
40
+ */
35
41
  declare class StitchDB {
36
42
  private url;
37
43
  private wsUrl;
@@ -40,38 +46,31 @@ declare class StitchDB {
40
46
  private pending;
41
47
  private msgId;
42
48
  private connecting;
43
- private useWebSocket;
49
+ private wsSupported;
50
+ private wsFailed;
44
51
  constructor(config: StitchDBConfig);
45
52
  private connect;
46
53
  private wsSend;
47
- private httpQuery;
48
- /** Run a SQL query with parameterized bindings. */
54
+ private httpPost;
55
+ private send;
49
56
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
50
- /** Run multiple queries atomically in a single batch. */
51
57
  batch(queries: {
52
58
  sql: string;
53
59
  params?: unknown[];
54
60
  }[]): Promise<BatchResult>;
55
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
56
61
  run(sql: string): Promise<ExecResult>;
57
- /** Insert a row. */
58
62
  insert(table: string, data: Record<string, unknown>): Promise<QueryResult>;
59
- /** Update rows matching a WHERE clause. */
63
+ insertMany(table: string, rows: Record<string, unknown>[]): Promise<BatchResult>;
60
64
  update(table: string, data: Record<string, unknown>, where: string, whereParams?: unknown[]): Promise<QueryResult>;
61
- /** Delete rows matching a WHERE clause. */
62
65
  remove(table: string, where: string, whereParams?: unknown[]): Promise<QueryResult>;
63
- /** Find one row by ID. */
64
66
  find<T = Record<string, unknown>>(table: string, id: unknown, idColumn?: string): Promise<T | null>;
65
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
66
67
  select<T = Record<string, unknown>>(table: string, where?: string, params?: unknown[], opts?: {
67
68
  orderBy?: string;
68
69
  limit?: number;
69
70
  offset?: number;
70
71
  }): Promise<T[]>;
71
- /** Close the WebSocket connection. */
72
72
  close(): void;
73
73
  }
74
- /** Create a StitchDB client. */
75
74
  declare function createClient(config: StitchDBConfig): StitchDB;
76
75
 
77
76
  export { type BatchResult, type ExecResult, type QueryResult, StitchDB, type StitchDBConfig, StitchDBError, createClient, StitchDB as default };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,12 @@ declare class StitchDBError extends Error {
32
32
  status: number;
33
33
  constructor(message: string, status?: number);
34
34
  }
35
+ /**
36
+ * StitchDB client.
37
+ *
38
+ * Uses WebSocket for queries (1 connection, unlimited queries, $0.019/M cost).
39
+ * Falls back to HTTP if WebSocket unavailable.
40
+ */
35
41
  declare class StitchDB {
36
42
  private url;
37
43
  private wsUrl;
@@ -40,38 +46,31 @@ declare class StitchDB {
40
46
  private pending;
41
47
  private msgId;
42
48
  private connecting;
43
- private useWebSocket;
49
+ private wsSupported;
50
+ private wsFailed;
44
51
  constructor(config: StitchDBConfig);
45
52
  private connect;
46
53
  private wsSend;
47
- private httpQuery;
48
- /** Run a SQL query with parameterized bindings. */
54
+ private httpPost;
55
+ private send;
49
56
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
50
- /** Run multiple queries atomically in a single batch. */
51
57
  batch(queries: {
52
58
  sql: string;
53
59
  params?: unknown[];
54
60
  }[]): Promise<BatchResult>;
55
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
56
61
  run(sql: string): Promise<ExecResult>;
57
- /** Insert a row. */
58
62
  insert(table: string, data: Record<string, unknown>): Promise<QueryResult>;
59
- /** Update rows matching a WHERE clause. */
63
+ insertMany(table: string, rows: Record<string, unknown>[]): Promise<BatchResult>;
60
64
  update(table: string, data: Record<string, unknown>, where: string, whereParams?: unknown[]): Promise<QueryResult>;
61
- /** Delete rows matching a WHERE clause. */
62
65
  remove(table: string, where: string, whereParams?: unknown[]): Promise<QueryResult>;
63
- /** Find one row by ID. */
64
66
  find<T = Record<string, unknown>>(table: string, id: unknown, idColumn?: string): Promise<T | null>;
65
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
66
67
  select<T = Record<string, unknown>>(table: string, where?: string, params?: unknown[], opts?: {
67
68
  orderBy?: string;
68
69
  limit?: number;
69
70
  offset?: number;
70
71
  }): Promise<T[]>;
71
- /** Close the WebSocket connection. */
72
72
  close(): void;
73
73
  }
74
- /** Create a StitchDB client. */
75
74
  declare function createClient(config: StitchDBConfig): StitchDB;
76
75
 
77
76
  export { type BatchResult, type ExecResult, type QueryResult, StitchDB, type StitchDBConfig, StitchDBError, createClient, StitchDB as default };
package/dist/index.js CHANGED
@@ -39,11 +39,13 @@ var StitchDB = class {
39
39
  this.pending = /* @__PURE__ */ new Map();
40
40
  this.msgId = 0;
41
41
  this.connecting = null;
42
+ this.wsFailed = false;
42
43
  this.url = (config.url || "https://db.stitchdb.com").replace(/\/$/, "");
43
44
  this.wsUrl = this.url.replace("https://", "wss://").replace("http://", "ws://");
44
45
  this.apiKey = config.apiKey;
45
- this.useWebSocket = typeof WebSocket !== "undefined";
46
+ this.wsSupported = typeof WebSocket !== "undefined";
46
47
  }
48
+ // ---- WebSocket ----
47
49
  async connect() {
48
50
  if (this.ws?.readyState === 1) return;
49
51
  if (this.connecting) return this.connecting;
@@ -55,17 +57,13 @@ var StitchDB = class {
55
57
  this.connecting = null;
56
58
  resolve();
57
59
  };
58
- ws.onmessage = (event) => {
60
+ ws.onmessage = (e) => {
59
61
  try {
60
- const data = JSON.parse(typeof event.data === "string" ? event.data : "");
61
- const pending = this.pending.get(data.id);
62
- if (pending) {
62
+ const data = JSON.parse(typeof e.data === "string" ? e.data : "");
63
+ const p = this.pending.get(data.id);
64
+ if (p) {
63
65
  this.pending.delete(data.id);
64
- if (data.error) {
65
- pending.reject(new StitchDBError(data.error));
66
- } else {
67
- pending.resolve(data);
68
- }
66
+ data.error ? p.reject(new StitchDBError(data.error)) : p.resolve(data);
69
67
  }
70
68
  } catch {
71
69
  }
@@ -73,29 +71,29 @@ var StitchDB = class {
73
71
  ws.onclose = () => {
74
72
  this.ws = null;
75
73
  this.connecting = null;
76
- for (const [id, p] of this.pending) {
77
- p.reject(new StitchDBError("Connection closed"));
78
- this.pending.delete(id);
79
- }
74
+ for (const [, p] of this.pending) p.reject(new StitchDBError("Connection closed"));
75
+ this.pending.clear();
80
76
  };
81
77
  ws.onerror = () => {
82
78
  this.ws = null;
83
79
  this.connecting = null;
84
- reject(new StitchDBError("WebSocket connection failed"));
80
+ this.wsFailed = true;
81
+ resolve();
85
82
  };
86
83
  } catch {
87
84
  this.connecting = null;
88
- this.useWebSocket = false;
85
+ this.wsFailed = true;
89
86
  resolve();
90
87
  }
91
88
  });
92
89
  return this.connecting;
93
90
  }
94
91
  async wsSend(msg) {
92
+ if (this.wsFailed) return this.httpPost(msg);
95
93
  await this.connect();
96
94
  if (!this.ws || this.ws.readyState !== 1) {
97
- this.useWebSocket = false;
98
- return this.httpQuery(msg);
95
+ this.wsFailed = true;
96
+ return this.httpPost(msg);
99
97
  }
100
98
  const id = String(++this.msgId);
101
99
  msg.id = id;
@@ -114,12 +112,19 @@ var StitchDB = class {
114
112
  reject(e);
115
113
  }
116
114
  });
117
- this.ws.send(JSON.stringify(msg));
115
+ try {
116
+ this.ws.send(JSON.stringify(msg));
117
+ } catch {
118
+ clearTimeout(timeout);
119
+ this.pending.delete(id);
120
+ this.wsFailed = true;
121
+ this.httpPost(msg).then(resolve, reject);
122
+ }
118
123
  });
119
124
  }
120
- async httpQuery(msg) {
121
- let path = "/v1/query";
122
- let body = { sql: msg.sql, params: msg.params };
125
+ // ---- HTTP fallback ----
126
+ async httpPost(msg) {
127
+ let path = "/v1/query", body = { sql: msg.sql, params: msg.params };
123
128
  if (msg.action === "batch") {
124
129
  path = "/v1/batch";
125
130
  body = { queries: msg.queries };
@@ -129,60 +134,48 @@ var StitchDB = class {
129
134
  }
130
135
  const res = await fetch(`${this.url}${path}`, {
131
136
  method: "POST",
132
- headers: {
133
- "Authorization": `Bearer ${this.apiKey}`,
134
- "Content-Type": "application/json"
135
- },
137
+ headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
136
138
  body: JSON.stringify(body)
137
139
  });
138
140
  const data = await res.json();
139
- if (!res.ok || data.error) {
140
- throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
141
- }
141
+ if (!res.ok || data.error) throw new StitchDBError(data.error || `HTTP ${res.status}`, res.status);
142
142
  return data;
143
143
  }
144
- /** Run a SQL query with parameterized bindings. */
144
+ // ---- Public API ----
145
+ async send(action, data) {
146
+ const msg = { action, ...data };
147
+ if (this.wsSupported && !this.wsFailed) return this.wsSend(msg);
148
+ return this.httpPost(msg);
149
+ }
145
150
  async query(sql, params) {
146
- if (this.useWebSocket) {
147
- return this.wsSend({ action: "query", sql, params });
148
- }
149
- return this.httpQuery({ action: "query", sql, params });
151
+ return this.send("query", { sql, params });
150
152
  }
151
- /** Run multiple queries atomically in a single batch. */
152
153
  async batch(queries) {
153
- if (this.useWebSocket) {
154
- return this.wsSend({ action: "batch", queries });
155
- }
156
- return this.httpQuery({ action: "batch", queries });
154
+ return this.send("batch", { queries });
157
155
  }
158
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
159
156
  async run(sql) {
160
- if (this.useWebSocket) {
161
- return this.wsSend({ action: "exec", sql });
162
- }
163
- return this.httpQuery({ action: "exec", sql });
157
+ return this.send("exec", { sql });
164
158
  }
165
- /** Insert a row. */
166
159
  async insert(table, data) {
167
160
  const cols = Object.keys(data);
161
+ return this.query(`INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`, Object.values(data));
162
+ }
163
+ async insertMany(table, rows) {
164
+ if (!rows.length) return { results: [], meta: { rows_read: 0, rows_written: 0, duration_ms: 0, queries_count: 0 } };
165
+ const cols = Object.keys(rows[0]);
168
166
  const sql = `INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`;
169
- return this.query(sql, Object.values(data));
167
+ return this.batch(rows.map((row) => ({ sql, params: cols.map((c) => row[c]) })));
170
168
  }
171
- /** Update rows matching a WHERE clause. */
172
169
  async update(table, data, where, whereParams) {
173
- const set = Object.keys(data).map((c) => `"${c}" = ?`).join(", ");
174
- return this.query(`UPDATE "${table}" SET ${set} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
170
+ return this.query(`UPDATE "${table}" SET ${Object.keys(data).map((c) => `"${c}" = ?`).join(", ")} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
175
171
  }
176
- /** Delete rows matching a WHERE clause. */
177
172
  async remove(table, where, whereParams) {
178
173
  return this.query(`DELETE FROM "${table}" WHERE ${where}`, whereParams);
179
174
  }
180
- /** Find one row by ID. */
181
175
  async find(table, id, idColumn = "id") {
182
176
  const { results } = await this.query(`SELECT * FROM "${table}" WHERE "${idColumn}" = ? LIMIT 1`, [id]);
183
177
  return results[0] || null;
184
178
  }
185
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
186
179
  async select(table, where, params, opts) {
187
180
  let sql = `SELECT * FROM "${table}"`;
188
181
  if (where) sql += ` WHERE ${where}`;
@@ -192,7 +185,6 @@ var StitchDB = class {
192
185
  const { results } = await this.query(sql, params);
193
186
  return results;
194
187
  }
195
- /** Close the WebSocket connection. */
196
188
  close() {
197
189
  if (this.ws) {
198
190
  this.ws.close();
package/dist/index.mjs CHANGED
@@ -12,11 +12,13 @@ var StitchDB = class {
12
12
  this.pending = /* @__PURE__ */ new Map();
13
13
  this.msgId = 0;
14
14
  this.connecting = null;
15
+ this.wsFailed = false;
15
16
  this.url = (config.url || "https://db.stitchdb.com").replace(/\/$/, "");
16
17
  this.wsUrl = this.url.replace("https://", "wss://").replace("http://", "ws://");
17
18
  this.apiKey = config.apiKey;
18
- this.useWebSocket = typeof WebSocket !== "undefined";
19
+ this.wsSupported = typeof WebSocket !== "undefined";
19
20
  }
21
+ // ---- WebSocket ----
20
22
  async connect() {
21
23
  if (this.ws?.readyState === 1) return;
22
24
  if (this.connecting) return this.connecting;
@@ -28,17 +30,13 @@ var StitchDB = class {
28
30
  this.connecting = null;
29
31
  resolve();
30
32
  };
31
- ws.onmessage = (event) => {
33
+ ws.onmessage = (e) => {
32
34
  try {
33
- const data = JSON.parse(typeof event.data === "string" ? event.data : "");
34
- const pending = this.pending.get(data.id);
35
- if (pending) {
35
+ const data = JSON.parse(typeof e.data === "string" ? e.data : "");
36
+ const p = this.pending.get(data.id);
37
+ if (p) {
36
38
  this.pending.delete(data.id);
37
- if (data.error) {
38
- pending.reject(new StitchDBError(data.error));
39
- } else {
40
- pending.resolve(data);
41
- }
39
+ data.error ? p.reject(new StitchDBError(data.error)) : p.resolve(data);
42
40
  }
43
41
  } catch {
44
42
  }
@@ -46,29 +44,29 @@ var StitchDB = class {
46
44
  ws.onclose = () => {
47
45
  this.ws = null;
48
46
  this.connecting = null;
49
- for (const [id, p] of this.pending) {
50
- p.reject(new StitchDBError("Connection closed"));
51
- this.pending.delete(id);
52
- }
47
+ for (const [, p] of this.pending) p.reject(new StitchDBError("Connection closed"));
48
+ this.pending.clear();
53
49
  };
54
50
  ws.onerror = () => {
55
51
  this.ws = null;
56
52
  this.connecting = null;
57
- reject(new StitchDBError("WebSocket connection failed"));
53
+ this.wsFailed = true;
54
+ resolve();
58
55
  };
59
56
  } catch {
60
57
  this.connecting = null;
61
- this.useWebSocket = false;
58
+ this.wsFailed = true;
62
59
  resolve();
63
60
  }
64
61
  });
65
62
  return this.connecting;
66
63
  }
67
64
  async wsSend(msg) {
65
+ if (this.wsFailed) return this.httpPost(msg);
68
66
  await this.connect();
69
67
  if (!this.ws || this.ws.readyState !== 1) {
70
- this.useWebSocket = false;
71
- return this.httpQuery(msg);
68
+ this.wsFailed = true;
69
+ return this.httpPost(msg);
72
70
  }
73
71
  const id = String(++this.msgId);
74
72
  msg.id = id;
@@ -87,12 +85,19 @@ var StitchDB = class {
87
85
  reject(e);
88
86
  }
89
87
  });
90
- this.ws.send(JSON.stringify(msg));
88
+ try {
89
+ this.ws.send(JSON.stringify(msg));
90
+ } catch {
91
+ clearTimeout(timeout);
92
+ this.pending.delete(id);
93
+ this.wsFailed = true;
94
+ this.httpPost(msg).then(resolve, reject);
95
+ }
91
96
  });
92
97
  }
93
- async httpQuery(msg) {
94
- let path = "/v1/query";
95
- let body = { sql: msg.sql, params: msg.params };
98
+ // ---- HTTP fallback ----
99
+ async httpPost(msg) {
100
+ let path = "/v1/query", body = { sql: msg.sql, params: msg.params };
96
101
  if (msg.action === "batch") {
97
102
  path = "/v1/batch";
98
103
  body = { queries: msg.queries };
@@ -102,60 +107,48 @@ var StitchDB = class {
102
107
  }
103
108
  const res = await fetch(`${this.url}${path}`, {
104
109
  method: "POST",
105
- headers: {
106
- "Authorization": `Bearer ${this.apiKey}`,
107
- "Content-Type": "application/json"
108
- },
110
+ headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
109
111
  body: JSON.stringify(body)
110
112
  });
111
113
  const data = await res.json();
112
- if (!res.ok || data.error) {
113
- throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
114
- }
114
+ if (!res.ok || data.error) throw new StitchDBError(data.error || `HTTP ${res.status}`, res.status);
115
115
  return data;
116
116
  }
117
- /** Run a SQL query with parameterized bindings. */
117
+ // ---- Public API ----
118
+ async send(action, data) {
119
+ const msg = { action, ...data };
120
+ if (this.wsSupported && !this.wsFailed) return this.wsSend(msg);
121
+ return this.httpPost(msg);
122
+ }
118
123
  async query(sql, params) {
119
- if (this.useWebSocket) {
120
- return this.wsSend({ action: "query", sql, params });
121
- }
122
- return this.httpQuery({ action: "query", sql, params });
124
+ return this.send("query", { sql, params });
123
125
  }
124
- /** Run multiple queries atomically in a single batch. */
125
126
  async batch(queries) {
126
- if (this.useWebSocket) {
127
- return this.wsSend({ action: "batch", queries });
128
- }
129
- return this.httpQuery({ action: "batch", queries });
127
+ return this.send("batch", { queries });
130
128
  }
131
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
132
129
  async run(sql) {
133
- if (this.useWebSocket) {
134
- return this.wsSend({ action: "exec", sql });
135
- }
136
- return this.httpQuery({ action: "exec", sql });
130
+ return this.send("exec", { sql });
137
131
  }
138
- /** Insert a row. */
139
132
  async insert(table, data) {
140
133
  const cols = Object.keys(data);
134
+ return this.query(`INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`, Object.values(data));
135
+ }
136
+ async insertMany(table, rows) {
137
+ if (!rows.length) return { results: [], meta: { rows_read: 0, rows_written: 0, duration_ms: 0, queries_count: 0 } };
138
+ const cols = Object.keys(rows[0]);
141
139
  const sql = `INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`;
142
- return this.query(sql, Object.values(data));
140
+ return this.batch(rows.map((row) => ({ sql, params: cols.map((c) => row[c]) })));
143
141
  }
144
- /** Update rows matching a WHERE clause. */
145
142
  async update(table, data, where, whereParams) {
146
- const set = Object.keys(data).map((c) => `"${c}" = ?`).join(", ");
147
- return this.query(`UPDATE "${table}" SET ${set} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
143
+ return this.query(`UPDATE "${table}" SET ${Object.keys(data).map((c) => `"${c}" = ?`).join(", ")} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
148
144
  }
149
- /** Delete rows matching a WHERE clause. */
150
145
  async remove(table, where, whereParams) {
151
146
  return this.query(`DELETE FROM "${table}" WHERE ${where}`, whereParams);
152
147
  }
153
- /** Find one row by ID. */
154
148
  async find(table, id, idColumn = "id") {
155
149
  const { results } = await this.query(`SELECT * FROM "${table}" WHERE "${idColumn}" = ? LIMIT 1`, [id]);
156
150
  return results[0] || null;
157
151
  }
158
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
159
152
  async select(table, where, params, opts) {
160
153
  let sql = `SELECT * FROM "${table}"`;
161
154
  if (where) sql += ` WHERE ${where}`;
@@ -165,7 +158,6 @@ var StitchDB = class {
165
158
  const { results } = await this.query(sql, params);
166
159
  return results;
167
160
  }
168
- /** Close the WebSocket connection. */
169
161
  close() {
170
162
  if (this.ws) {
171
163
  this.ws.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchdb",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "StitchDB client for JavaScript and TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",