sqlite-hub-client 0.5.0 → 0.7.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 CHANGED
@@ -73,6 +73,26 @@ const active = await db.count("users", { active: 1 });
73
73
  // Exists check
74
74
  const exists = await db.exists("users", { email: "alice@example.com" });
75
75
 
76
+ // Upsert — INSERT OR REPLACE (relies on PRIMARY KEY / UNIQUE constraints)
77
+ await db.upsert("users", { id: 1, email: "alice@example.com", name: "Alice" });
78
+
79
+ // Upsert — explicit conflict target
80
+ await db.upsert(
81
+ "users",
82
+ { email: "alice@example.com", name: "Alice" },
83
+ { conflictColumns: ["email"] }
84
+ );
85
+
86
+ // Bulk upsert
87
+ await db.upsertMany(
88
+ "users",
89
+ [
90
+ { email: "alice@example.com", name: "Alice" },
91
+ { email: "bob@example.com", name: "Bob" },
92
+ ],
93
+ { conflictColumns: ["email"] }
94
+ );
95
+
76
96
  // Update
77
97
  await db.update("users", { name: "Alice Smith" }, { id: 1 });
78
98
 
@@ -168,6 +188,46 @@ await db.insertMany("posts", [
168
188
  ]);
169
189
  ```
170
190
 
191
+ #### `db.upsert(table, data, options?)` → `ExecResult`
192
+
193
+ Insert a row or update it on conflict.
194
+
195
+ ```ts
196
+ // INSERT OR REPLACE — relies on PRIMARY KEY / UNIQUE constraints
197
+ await db.upsert("users", { id: 1, email: "alice@example.com", name: "Alice" });
198
+
199
+ // INSERT … ON CONFLICT(email) DO UPDATE SET name = excluded.name
200
+ await db.upsert(
201
+ "users",
202
+ { email: "alice@example.com", name: "Alice" },
203
+ { conflictColumns: ["email"] }
204
+ );
205
+ ```
206
+
207
+ #### `db.upsertMany(table, rows, options?)` → `ExecResult`
208
+
209
+ Bulk version of `upsert`.
210
+
211
+ ```ts
212
+ await db.upsertMany(
213
+ "users",
214
+ [
215
+ { email: "alice@example.com", name: "Alice" },
216
+ { email: "bob@example.com", name: "Bob" },
217
+ ],
218
+ { conflictColumns: ["email"] }
219
+ );
220
+ ```
221
+
222
+ **`UpsertOptions` fields:**
223
+
224
+ | Field | Type | Description |
225
+ | ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------- |
226
+ | `conflictColumns` | `string[]` | Conflict-target columns. Omit to use `INSERT OR REPLACE` (relies on PRIMARY KEY / UNIQUE). |
227
+ | `updateColumns` | `string[]` | Columns to overwrite on conflict. Defaults to all columns **not** in `conflictColumns`. Pass `[]` for DO NOTHING. |
228
+
229
+ ---
230
+
171
231
  #### `db.update(table, data, where)` → `ExecResult`
172
232
 
173
233
  ```ts
@@ -21,11 +21,11 @@ export class HttpAdapter {
21
21
  sql,
22
22
  ...(bindings?.length ? { bindings } : {}),
23
23
  });
24
- if (res.isError()) {
25
- const body = res.data;
24
+ const body = res.data;
25
+ if (res.isError() || body == null) {
26
26
  throw new Error(body?.error ??
27
- `sqlite-hub: HTTP ${res.status} for db "${this.options.db}"`);
27
+ `sqlite-hub: HTTP ${res.status} no response body (db: "${this.options.db}")`);
28
28
  }
29
- return res.data;
29
+ return body;
30
30
  }
31
31
  }
@@ -25,6 +25,21 @@ export interface CreateIndexOptions {
25
25
  unique?: boolean;
26
26
  ifNotExists?: boolean;
27
27
  }
28
+ export interface UpsertOptions {
29
+ /**
30
+ * Column(s) that form the conflict target.
31
+ * When provided the statement becomes:
32
+ * INSERT INTO … ON CONFLICT(col1, col2) DO UPDATE SET …
33
+ * When omitted `INSERT OR REPLACE` is used instead (relies on the
34
+ * table's PRIMARY KEY / UNIQUE constraints).
35
+ */
36
+ conflictColumns?: string[];
37
+ /**
38
+ * Columns to update on conflict.
39
+ * Defaults to all columns that are NOT in `conflictColumns`.
40
+ */
41
+ updateColumns?: string[];
42
+ }
28
43
  /**
29
44
  * High-level database client.
30
45
  * Accepts any `IAdapter` — currently `HttpAdapter`, extensible to direct
@@ -76,6 +91,37 @@ export declare class Database {
76
91
  * ]);
77
92
  */
78
93
  insertMany(table: string, rows: Record<string, unknown>[]): Promise<ExecResult>;
94
+ /**
95
+ * Upsert a single row — insert or update on conflict.
96
+ *
97
+ * Without `conflictColumns` the statement uses `INSERT OR REPLACE` which
98
+ * relies on the table's PRIMARY KEY / UNIQUE constraints.
99
+ *
100
+ * With `conflictColumns` it uses the more precise:
101
+ * `INSERT … ON CONFLICT(cols) DO UPDATE SET …`
102
+ *
103
+ * @example
104
+ * // relies on PRIMARY KEY / UNIQUE constraints
105
+ * await db.upsert("users", { id: 1, email: "a@b.com", name: "Alice" });
106
+ *
107
+ * // explicit conflict target
108
+ * await db.upsert("users", { email: "a@b.com", name: "Alice" }, { conflictColumns: ["email"] });
109
+ */
110
+ upsert(table: string, data: Record<string, unknown>, options?: UpsertOptions): Promise<ExecResult>;
111
+ /**
112
+ * Upsert multiple rows — insert or update each row on conflict.
113
+ *
114
+ * @example
115
+ * await db.upsertMany(
116
+ * "users",
117
+ * [
118
+ * { email: "a@b.com", name: "Alice" },
119
+ * { email: "b@c.com", name: "Bob" },
120
+ * ],
121
+ * { conflictColumns: ["email"] }
122
+ * );
123
+ */
124
+ upsertMany(table: string, rows: Record<string, unknown>[], options?: UpsertOptions): Promise<ExecResult>;
79
125
  /**
80
126
  * Update rows matching `where`.
81
127
  *
package/dist/database.js CHANGED
@@ -13,7 +13,7 @@ function isQueryResult(r) {
13
13
  return "rows" in r;
14
14
  }
15
15
  function isExecResult(r) {
16
- return "rowsAffected" in r;
16
+ return r != null && "rowsAffected" in r;
17
17
  }
18
18
  // ── Database ─────────────────────────────────────────────────────────────────
19
19
  /**
@@ -114,6 +114,62 @@ export class Database {
114
114
  const bindings = rows.flatMap((row) => keys.map((k) => row[k]));
115
115
  return this._write(`INSERT INTO "${table}" (${cols}) VALUES ${rowPlaceholders}`, bindings);
116
116
  }
117
+ /**
118
+ * Upsert a single row — insert or update on conflict.
119
+ *
120
+ * Without `conflictColumns` the statement uses `INSERT OR REPLACE` which
121
+ * relies on the table's PRIMARY KEY / UNIQUE constraints.
122
+ *
123
+ * With `conflictColumns` it uses the more precise:
124
+ * `INSERT … ON CONFLICT(cols) DO UPDATE SET …`
125
+ *
126
+ * @example
127
+ * // relies on PRIMARY KEY / UNIQUE constraints
128
+ * await db.upsert("users", { id: 1, email: "a@b.com", name: "Alice" });
129
+ *
130
+ * // explicit conflict target
131
+ * await db.upsert("users", { email: "a@b.com", name: "Alice" }, { conflictColumns: ["email"] });
132
+ */
133
+ async upsert(table, data, options = {}) {
134
+ return this.upsertMany(table, [data], options);
135
+ }
136
+ /**
137
+ * Upsert multiple rows — insert or update each row on conflict.
138
+ *
139
+ * @example
140
+ * await db.upsertMany(
141
+ * "users",
142
+ * [
143
+ * { email: "a@b.com", name: "Alice" },
144
+ * { email: "b@c.com", name: "Bob" },
145
+ * ],
146
+ * { conflictColumns: ["email"] }
147
+ * );
148
+ */
149
+ async upsertMany(table, rows, options = {}) {
150
+ if (rows.length === 0)
151
+ return { rowsAffected: 0, lastInsertRowid: null };
152
+ const keys = Object.keys(rows[0]);
153
+ const cols = keys.map((k) => `"${k}"`).join(", ");
154
+ const rowPlaceholders = rows
155
+ .map(() => `(${keys.map(() => "?").join(", ")})`)
156
+ .join(", ");
157
+ const bindings = rows.flatMap((row) => keys.map((k) => row[k]));
158
+ if (options.conflictColumns && options.conflictColumns.length > 0) {
159
+ const conflictCols = options.conflictColumns
160
+ .map((c) => `"${c}"`)
161
+ .join(", ");
162
+ const updateCols = options.updateColumns ??
163
+ keys.filter((k) => !options.conflictColumns.includes(k));
164
+ const conflictClause = updateCols.length === 0
165
+ ? `ON CONFLICT(${conflictCols}) DO NOTHING`
166
+ : `ON CONFLICT(${conflictCols}) DO UPDATE SET ${updateCols
167
+ .map((k) => `"${k}" = excluded."${k}"`)
168
+ .join(", ")}`;
169
+ return this._write(`INSERT INTO "${table}" (${cols}) VALUES ${rowPlaceholders} ${conflictClause}`, bindings);
170
+ }
171
+ return this._write(`INSERT OR REPLACE INTO "${table}" (${cols}) VALUES ${rowPlaceholders}`, bindings);
172
+ }
117
173
  /**
118
174
  * Update rows matching `where`.
119
175
  *
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Database } from "./database.js";
2
- export type { ColumnDef, CreateIndexOptions, CreateTableOptions, FindOptions, OrderDirection, WhereClause } from "./database.js";
2
+ export type { ColumnDef, CreateIndexOptions, CreateTableOptions, FindOptions, OrderDirection, UpsertOptions, WhereClause } from "./database.js";
3
3
  export { HttpAdapter } from "./adapters/http.js";
4
4
  export type { HttpAdapterOptions } from "./adapters/http.js";
5
5
  export type { ColumnHeader, ExecResult, IAdapter, QueryResult, RawResult } from "./adapters/types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub-client",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "High-level SQLite client for sqlite-hub — HTTP adapter included, direct SQLite coming soon",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",