@objectstack/driver-sql 3.2.9 → 3.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.mjs CHANGED
@@ -4,21 +4,47 @@ import { nanoid } from "nanoid";
4
4
  var DEFAULT_ID_LENGTH = 16;
5
5
  var SqlDriver = class {
6
6
  constructor(config) {
7
- // DriverInterface metadata
7
+ // IDataDriver metadata
8
8
  this.name = "com.objectstack.driver.sql";
9
9
  this.version = "1.0.0";
10
10
  this.supports = {
11
+ // Basic CRUD Operations
12
+ create: true,
13
+ read: true,
14
+ update: true,
15
+ delete: true,
16
+ // Bulk Operations
17
+ bulkCreate: true,
18
+ bulkUpdate: true,
19
+ bulkDelete: true,
20
+ // Transaction & Connection Management
11
21
  transactions: true,
12
- joins: true,
13
- fullTextSearch: false,
14
- jsonFields: true,
15
- arrayFields: true,
22
+ savepoints: false,
23
+ // Query Operations
16
24
  queryFilters: true,
17
25
  queryAggregations: true,
18
26
  querySorting: true,
19
27
  queryPagination: true,
20
28
  queryWindowFunctions: true,
21
- querySubqueries: true
29
+ querySubqueries: true,
30
+ queryCTE: false,
31
+ joins: true,
32
+ // Advanced Features
33
+ fullTextSearch: false,
34
+ jsonQuery: false,
35
+ geospatialQuery: false,
36
+ streaming: false,
37
+ jsonFields: true,
38
+ arrayFields: true,
39
+ vectorSearch: false,
40
+ // Schema Management
41
+ schemaSync: true,
42
+ migrations: false,
43
+ indexes: false,
44
+ // Performance & Optimization
45
+ connectionPooling: true,
46
+ preparedStatements: true,
47
+ queryCache: false
22
48
  };
23
49
  this.jsonFields = {};
24
50
  this.booleanFields = {};
@@ -68,24 +94,18 @@ var SqlDriver = class {
68
94
  } else {
69
95
  builder.select("*");
70
96
  }
71
- const filterCondition = query.where || query.filters;
72
- if (filterCondition) {
73
- this.applyFilters(builder, filterCondition);
74
- }
75
- const sortArray = query.orderBy || query.sort;
76
- if (sortArray && Array.isArray(sortArray)) {
77
- for (const item of sortArray) {
78
- const field = item.field || item[0];
79
- const dir = item.order || item[1] || "asc";
80
- if (field) {
81
- builder.orderBy(this.mapSortField(field), dir);
97
+ if (query.where) {
98
+ this.applyFilters(builder, query.where);
99
+ }
100
+ if (query.orderBy && Array.isArray(query.orderBy)) {
101
+ for (const item of query.orderBy) {
102
+ if (item.field) {
103
+ builder.orderBy(this.mapSortField(item.field), item.order || "asc");
82
104
  }
83
105
  }
84
106
  }
85
- const offsetValue = query.offset ?? query.skip;
86
- const limitValue = query.limit ?? query.top;
87
- if (offsetValue !== void 0) builder.offset(offsetValue);
88
- if (limitValue !== void 0) builder.limit(limitValue);
107
+ if (query.offset !== void 0) builder.offset(query.offset);
108
+ if (query.limit !== void 0) builder.limit(query.limit);
89
109
  let results;
90
110
  try {
91
111
  results = await builder;
@@ -116,6 +136,17 @@ var SqlDriver = class {
116
136
  }
117
137
  return null;
118
138
  }
139
+ /**
140
+ * Stream records matching a structured query.
141
+ * NOTE: Current implementation fetches all results then yields them.
142
+ * TODO: Use Knex .stream() for true cursor-based streaming on large datasets.
143
+ */
144
+ async *findStream(object, query, options) {
145
+ const results = await this.find(object, query, options);
146
+ for (const row of results) {
147
+ yield row;
148
+ }
149
+ }
119
150
  async create(object, data, options) {
120
151
  const { _id, ...rest } = data;
121
152
  const toInsert = { ...rest };
@@ -144,44 +175,71 @@ var SqlDriver = class {
144
175
  const updated = await this.getBuilder(object, options).where("id", id).first();
145
176
  return this.formatOutput(object, updated) || null;
146
177
  }
178
+ async upsert(object, data, conflictKeys, options) {
179
+ const { _id, ...rest } = data;
180
+ const toUpsert = { ...rest };
181
+ if (_id !== void 0 && toUpsert.id === void 0) {
182
+ toUpsert.id = _id;
183
+ } else if (toUpsert.id === void 0) {
184
+ toUpsert.id = nanoid(DEFAULT_ID_LENGTH);
185
+ }
186
+ const formatted = this.formatInput(object, toUpsert);
187
+ const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
188
+ const builder = this.getBuilder(object, options);
189
+ await builder.insert(formatted).onConflict(mergeKeys).merge();
190
+ const result = await this.getBuilder(object, options).where("id", toUpsert.id).first();
191
+ return this.formatOutput(object, result) || toUpsert;
192
+ }
147
193
  async delete(object, id, options) {
148
194
  const builder = this.getBuilder(object, options);
149
- return await builder.where("id", id).delete();
195
+ const count = await builder.where("id", id).delete();
196
+ return count > 0;
150
197
  }
151
198
  // ===================================
152
- // Optional bulk & batch
199
+ // Bulk & Batch Operations
153
200
  // ===================================
154
201
  async bulkCreate(object, data, options) {
155
202
  const builder = this.getBuilder(object, options);
156
203
  return await builder.insert(data).returning("*");
157
204
  }
205
+ /**
206
+ * Batch-update multiple records by ID.
207
+ * NOTE: Current implementation performs sequential updates for correctness.
208
+ * TODO: Optimize with SQL CASE statements or batched transactions for performance.
209
+ */
210
+ async bulkUpdate(object, updates, options) {
211
+ const results = [];
212
+ for (const { id, data } of updates) {
213
+ const updated = await this.update(object, id, data, options);
214
+ if (updated) results.push(updated);
215
+ }
216
+ return results;
217
+ }
218
+ async bulkDelete(object, ids, options) {
219
+ const builder = this.getBuilder(object, options);
220
+ await builder.whereIn("id", ids).delete();
221
+ }
158
222
  async updateMany(object, query, data, options) {
159
223
  const builder = this.getBuilder(object, options);
160
- const filters = query.where || query.filters || query;
161
- if (filters) this.applyFilters(builder, filters);
224
+ if (query.where) this.applyFilters(builder, query.where);
162
225
  const count = await builder.update(data);
163
- return { modifiedCount: count || 0 };
226
+ return count || 0;
164
227
  }
165
228
  async deleteMany(object, query, options) {
166
229
  const builder = this.getBuilder(object, options);
167
- const filters = query.where || query.filters || query;
168
- if (filters) this.applyFilters(builder, filters);
230
+ if (query.where) this.applyFilters(builder, query.where);
169
231
  const count = await builder.delete();
170
- return { deletedCount: count || 0 };
232
+ return count || 0;
171
233
  }
172
234
  async count(object, query, options) {
173
235
  const builder = this.getBuilder(object, options);
174
- let actualFilters = query;
175
- if (query && (query.where || query.filters)) {
176
- actualFilters = query.where || query.filters;
177
- }
178
- if (actualFilters) {
179
- this.applyFilters(builder, actualFilters);
236
+ if (query?.where) {
237
+ this.applyFilters(builder, query.where);
180
238
  }
181
239
  const result = await builder.count("* as count");
182
240
  if (result && result.length > 0) {
183
241
  const row = result[0];
184
- return Number(row.count || row["count(*)"]);
242
+ return Number(row.count ?? row["count(*)"] ?? 0);
185
243
  }
186
244
  return 0;
187
245
  }
@@ -201,11 +259,21 @@ var SqlDriver = class {
201
259
  async beginTransaction() {
202
260
  return await this.knex.transaction();
203
261
  }
262
+ /** IDataDriver standard */
263
+ async commit(transaction) {
264
+ await transaction.commit();
265
+ }
266
+ /** IDataDriver standard */
267
+ async rollback(transaction) {
268
+ await transaction.rollback();
269
+ }
270
+ /** @deprecated Use commit() instead */
204
271
  async commitTransaction(trx) {
205
- await trx.commit();
272
+ await this.commit(trx);
206
273
  }
274
+ /** @deprecated Use rollback() instead */
207
275
  async rollbackTransaction(trx) {
208
- await trx.rollback();
276
+ await this.rollback(trx);
209
277
  }
210
278
  // ===================================
211
279
  // Aggregation
@@ -274,6 +342,10 @@ var SqlDriver = class {
274
342
  // ===================================
275
343
  // Query Plan Analysis
276
344
  // ===================================
345
+ /** IDataDriver standard: analyze query performance */
346
+ async explain(object, query, options) {
347
+ return this.analyzeQuery(object, query, options);
348
+ }
277
349
  async analyzeQuery(object, query, options) {
278
350
  const builder = this.getBuilder(object, options);
279
351
  if (query.fields) {
@@ -325,7 +397,10 @@ var SqlDriver = class {
325
397
  // ===================================
326
398
  async syncSchema(object, schema, _options) {
327
399
  const objectDef = schema;
328
- await this.initObjects([objectDef]);
400
+ await this.initObjects([{ ...objectDef, name: object }]);
401
+ }
402
+ async dropTable(object, _options) {
403
+ await this.knex.schema.dropTableIfExists(object);
329
404
  }
330
405
  /**
331
406
  * Batch-initialise tables from an array of object definitions.
@@ -333,7 +408,7 @@ var SqlDriver = class {
333
408
  async initObjects(objects) {
334
409
  await this.ensureDatabaseExists();
335
410
  for (const obj of objects) {
336
- const tableName = obj.name;
411
+ const tableName = obj.tableName || obj.name;
337
412
  const jsonCols = [];
338
413
  const booleanCols = [];
339
414
  if (obj.fields) {
@@ -358,6 +433,7 @@ var SqlDriver = class {
358
433
  exists = false;
359
434
  }
360
435
  }
436
+ const builtinColumns = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
361
437
  if (!exists) {
362
438
  await this.knex.schema.createTable(tableName, (table) => {
363
439
  table.string("id").primary();
@@ -365,6 +441,7 @@ var SqlDriver = class {
365
441
  table.timestamp("updated_at").defaultTo(this.knex.fn.now());
366
442
  if (obj.fields) {
367
443
  for (const [name, field] of Object.entries(obj.fields)) {
444
+ if (builtinColumns.has(name)) continue;
368
445
  this.createColumn(table, name, field);
369
446
  }
370
447
  }
@@ -458,7 +535,7 @@ var SqlDriver = class {
458
535
  return;
459
536
  }
460
537
  for (const [key, value] of Object.entries(filters)) {
461
- if (["filters", "sort", "limit", "skip", "offset", "fields", "orderBy"].includes(key)) continue;
538
+ if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
462
539
  builder.where(key, value);
463
540
  }
464
541
  return;
@@ -699,11 +776,12 @@ var SqlDriver = class {
699
776
  }
700
777
  // ── Database helpers ────────────────────────────────────────────────────────
701
778
  async ensureDatabaseExists() {
702
- if (!this.isPostgres) return;
779
+ if (this.isSqlite) return;
780
+ if (!this.isPostgres && !this.isMysql) return;
703
781
  try {
704
782
  await this.knex.raw("SELECT 1");
705
783
  } catch (e) {
706
- if (e.code === "3D000") {
784
+ if (e.code === "3D000" || e.code === "ER_BAD_DB_ERROR" || e.errno === 1049) {
707
785
  await this.createDatabase();
708
786
  } else {
709
787
  throw e;
@@ -715,18 +793,37 @@ var SqlDriver = class {
715
793
  const connection = config.connection;
716
794
  let dbName = "";
717
795
  const adminConfig = { ...config };
718
- if (typeof connection === "string") {
719
- const url = new URL(connection);
720
- dbName = url.pathname.slice(1);
721
- url.pathname = "/postgres";
722
- adminConfig.connection = url.toString();
796
+ if (this.isPostgres) {
797
+ if (typeof connection === "string") {
798
+ const url = new URL(connection);
799
+ dbName = url.pathname.slice(1);
800
+ url.pathname = "/postgres";
801
+ adminConfig.connection = url.toString();
802
+ } else {
803
+ dbName = connection.database;
804
+ adminConfig.connection = { ...connection, database: "postgres" };
805
+ }
806
+ } else if (this.isMysql) {
807
+ if (typeof connection === "string") {
808
+ const url = new URL(connection);
809
+ dbName = url.pathname.slice(1);
810
+ url.pathname = "/";
811
+ adminConfig.connection = url.toString();
812
+ } else {
813
+ dbName = connection.database;
814
+ const { database: _db, ...rest } = connection;
815
+ adminConfig.connection = rest;
816
+ }
723
817
  } else {
724
- dbName = connection.database;
725
- adminConfig.connection = { ...connection, database: "postgres" };
818
+ return;
726
819
  }
727
820
  const adminKnex = knex(adminConfig);
728
821
  try {
729
- await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
822
+ if (this.isPostgres) {
823
+ await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
824
+ } else if (this.isMysql) {
825
+ await adminKnex.raw(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
826
+ }
730
827
  } finally {
731
828
  await adminKnex.destroy();
732
829
  }