@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.js CHANGED
@@ -41,21 +41,47 @@ var import_nanoid = require("nanoid");
41
41
  var DEFAULT_ID_LENGTH = 16;
42
42
  var SqlDriver = class {
43
43
  constructor(config) {
44
- // DriverInterface metadata
44
+ // IDataDriver metadata
45
45
  this.name = "com.objectstack.driver.sql";
46
46
  this.version = "1.0.0";
47
47
  this.supports = {
48
+ // Basic CRUD Operations
49
+ create: true,
50
+ read: true,
51
+ update: true,
52
+ delete: true,
53
+ // Bulk Operations
54
+ bulkCreate: true,
55
+ bulkUpdate: true,
56
+ bulkDelete: true,
57
+ // Transaction & Connection Management
48
58
  transactions: true,
49
- joins: true,
50
- fullTextSearch: false,
51
- jsonFields: true,
52
- arrayFields: true,
59
+ savepoints: false,
60
+ // Query Operations
53
61
  queryFilters: true,
54
62
  queryAggregations: true,
55
63
  querySorting: true,
56
64
  queryPagination: true,
57
65
  queryWindowFunctions: true,
58
- querySubqueries: true
66
+ querySubqueries: true,
67
+ queryCTE: false,
68
+ joins: true,
69
+ // Advanced Features
70
+ fullTextSearch: false,
71
+ jsonQuery: false,
72
+ geospatialQuery: false,
73
+ streaming: false,
74
+ jsonFields: true,
75
+ arrayFields: true,
76
+ vectorSearch: false,
77
+ // Schema Management
78
+ schemaSync: true,
79
+ migrations: false,
80
+ indexes: false,
81
+ // Performance & Optimization
82
+ connectionPooling: true,
83
+ preparedStatements: true,
84
+ queryCache: false
59
85
  };
60
86
  this.jsonFields = {};
61
87
  this.booleanFields = {};
@@ -105,24 +131,18 @@ var SqlDriver = class {
105
131
  } else {
106
132
  builder.select("*");
107
133
  }
108
- const filterCondition = query.where || query.filters;
109
- if (filterCondition) {
110
- this.applyFilters(builder, filterCondition);
134
+ if (query.where) {
135
+ this.applyFilters(builder, query.where);
111
136
  }
112
- const sortArray = query.orderBy || query.sort;
113
- if (sortArray && Array.isArray(sortArray)) {
114
- for (const item of sortArray) {
115
- const field = item.field || item[0];
116
- const dir = item.order || item[1] || "asc";
117
- if (field) {
118
- builder.orderBy(this.mapSortField(field), dir);
137
+ if (query.orderBy && Array.isArray(query.orderBy)) {
138
+ for (const item of query.orderBy) {
139
+ if (item.field) {
140
+ builder.orderBy(this.mapSortField(item.field), item.order || "asc");
119
141
  }
120
142
  }
121
143
  }
122
- const offsetValue = query.offset ?? query.skip;
123
- const limitValue = query.limit ?? query.top;
124
- if (offsetValue !== void 0) builder.offset(offsetValue);
125
- if (limitValue !== void 0) builder.limit(limitValue);
144
+ if (query.offset !== void 0) builder.offset(query.offset);
145
+ if (query.limit !== void 0) builder.limit(query.limit);
126
146
  let results;
127
147
  try {
128
148
  results = await builder;
@@ -153,6 +173,17 @@ var SqlDriver = class {
153
173
  }
154
174
  return null;
155
175
  }
176
+ /**
177
+ * Stream records matching a structured query.
178
+ * NOTE: Current implementation fetches all results then yields them.
179
+ * TODO: Use Knex .stream() for true cursor-based streaming on large datasets.
180
+ */
181
+ async *findStream(object, query, options) {
182
+ const results = await this.find(object, query, options);
183
+ for (const row of results) {
184
+ yield row;
185
+ }
186
+ }
156
187
  async create(object, data, options) {
157
188
  const { _id, ...rest } = data;
158
189
  const toInsert = { ...rest };
@@ -181,44 +212,71 @@ var SqlDriver = class {
181
212
  const updated = await this.getBuilder(object, options).where("id", id).first();
182
213
  return this.formatOutput(object, updated) || null;
183
214
  }
215
+ async upsert(object, data, conflictKeys, options) {
216
+ const { _id, ...rest } = data;
217
+ const toUpsert = { ...rest };
218
+ if (_id !== void 0 && toUpsert.id === void 0) {
219
+ toUpsert.id = _id;
220
+ } else if (toUpsert.id === void 0) {
221
+ toUpsert.id = (0, import_nanoid.nanoid)(DEFAULT_ID_LENGTH);
222
+ }
223
+ const formatted = this.formatInput(object, toUpsert);
224
+ const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
225
+ const builder = this.getBuilder(object, options);
226
+ await builder.insert(formatted).onConflict(mergeKeys).merge();
227
+ const result = await this.getBuilder(object, options).where("id", toUpsert.id).first();
228
+ return this.formatOutput(object, result) || toUpsert;
229
+ }
184
230
  async delete(object, id, options) {
185
231
  const builder = this.getBuilder(object, options);
186
- return await builder.where("id", id).delete();
232
+ const count = await builder.where("id", id).delete();
233
+ return count > 0;
187
234
  }
188
235
  // ===================================
189
- // Optional bulk & batch
236
+ // Bulk & Batch Operations
190
237
  // ===================================
191
238
  async bulkCreate(object, data, options) {
192
239
  const builder = this.getBuilder(object, options);
193
240
  return await builder.insert(data).returning("*");
194
241
  }
242
+ /**
243
+ * Batch-update multiple records by ID.
244
+ * NOTE: Current implementation performs sequential updates for correctness.
245
+ * TODO: Optimize with SQL CASE statements or batched transactions for performance.
246
+ */
247
+ async bulkUpdate(object, updates, options) {
248
+ const results = [];
249
+ for (const { id, data } of updates) {
250
+ const updated = await this.update(object, id, data, options);
251
+ if (updated) results.push(updated);
252
+ }
253
+ return results;
254
+ }
255
+ async bulkDelete(object, ids, options) {
256
+ const builder = this.getBuilder(object, options);
257
+ await builder.whereIn("id", ids).delete();
258
+ }
195
259
  async updateMany(object, query, data, options) {
196
260
  const builder = this.getBuilder(object, options);
197
- const filters = query.where || query.filters || query;
198
- if (filters) this.applyFilters(builder, filters);
261
+ if (query.where) this.applyFilters(builder, query.where);
199
262
  const count = await builder.update(data);
200
- return { modifiedCount: count || 0 };
263
+ return count || 0;
201
264
  }
202
265
  async deleteMany(object, query, options) {
203
266
  const builder = this.getBuilder(object, options);
204
- const filters = query.where || query.filters || query;
205
- if (filters) this.applyFilters(builder, filters);
267
+ if (query.where) this.applyFilters(builder, query.where);
206
268
  const count = await builder.delete();
207
- return { deletedCount: count || 0 };
269
+ return count || 0;
208
270
  }
209
271
  async count(object, query, options) {
210
272
  const builder = this.getBuilder(object, options);
211
- let actualFilters = query;
212
- if (query && (query.where || query.filters)) {
213
- actualFilters = query.where || query.filters;
214
- }
215
- if (actualFilters) {
216
- this.applyFilters(builder, actualFilters);
273
+ if (query?.where) {
274
+ this.applyFilters(builder, query.where);
217
275
  }
218
276
  const result = await builder.count("* as count");
219
277
  if (result && result.length > 0) {
220
278
  const row = result[0];
221
- return Number(row.count || row["count(*)"]);
279
+ return Number(row.count ?? row["count(*)"] ?? 0);
222
280
  }
223
281
  return 0;
224
282
  }
@@ -238,11 +296,21 @@ var SqlDriver = class {
238
296
  async beginTransaction() {
239
297
  return await this.knex.transaction();
240
298
  }
299
+ /** IDataDriver standard */
300
+ async commit(transaction) {
301
+ await transaction.commit();
302
+ }
303
+ /** IDataDriver standard */
304
+ async rollback(transaction) {
305
+ await transaction.rollback();
306
+ }
307
+ /** @deprecated Use commit() instead */
241
308
  async commitTransaction(trx) {
242
- await trx.commit();
309
+ await this.commit(trx);
243
310
  }
311
+ /** @deprecated Use rollback() instead */
244
312
  async rollbackTransaction(trx) {
245
- await trx.rollback();
313
+ await this.rollback(trx);
246
314
  }
247
315
  // ===================================
248
316
  // Aggregation
@@ -311,6 +379,10 @@ var SqlDriver = class {
311
379
  // ===================================
312
380
  // Query Plan Analysis
313
381
  // ===================================
382
+ /** IDataDriver standard: analyze query performance */
383
+ async explain(object, query, options) {
384
+ return this.analyzeQuery(object, query, options);
385
+ }
314
386
  async analyzeQuery(object, query, options) {
315
387
  const builder = this.getBuilder(object, options);
316
388
  if (query.fields) {
@@ -362,7 +434,10 @@ var SqlDriver = class {
362
434
  // ===================================
363
435
  async syncSchema(object, schema, _options) {
364
436
  const objectDef = schema;
365
- await this.initObjects([objectDef]);
437
+ await this.initObjects([{ ...objectDef, name: object }]);
438
+ }
439
+ async dropTable(object, _options) {
440
+ await this.knex.schema.dropTableIfExists(object);
366
441
  }
367
442
  /**
368
443
  * Batch-initialise tables from an array of object definitions.
@@ -370,7 +445,7 @@ var SqlDriver = class {
370
445
  async initObjects(objects) {
371
446
  await this.ensureDatabaseExists();
372
447
  for (const obj of objects) {
373
- const tableName = obj.name;
448
+ const tableName = obj.tableName || obj.name;
374
449
  const jsonCols = [];
375
450
  const booleanCols = [];
376
451
  if (obj.fields) {
@@ -395,6 +470,7 @@ var SqlDriver = class {
395
470
  exists = false;
396
471
  }
397
472
  }
473
+ const builtinColumns = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
398
474
  if (!exists) {
399
475
  await this.knex.schema.createTable(tableName, (table) => {
400
476
  table.string("id").primary();
@@ -402,6 +478,7 @@ var SqlDriver = class {
402
478
  table.timestamp("updated_at").defaultTo(this.knex.fn.now());
403
479
  if (obj.fields) {
404
480
  for (const [name, field] of Object.entries(obj.fields)) {
481
+ if (builtinColumns.has(name)) continue;
405
482
  this.createColumn(table, name, field);
406
483
  }
407
484
  }
@@ -495,7 +572,7 @@ var SqlDriver = class {
495
572
  return;
496
573
  }
497
574
  for (const [key, value] of Object.entries(filters)) {
498
- if (["filters", "sort", "limit", "skip", "offset", "fields", "orderBy"].includes(key)) continue;
575
+ if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
499
576
  builder.where(key, value);
500
577
  }
501
578
  return;
@@ -736,11 +813,12 @@ var SqlDriver = class {
736
813
  }
737
814
  // ── Database helpers ────────────────────────────────────────────────────────
738
815
  async ensureDatabaseExists() {
739
- if (!this.isPostgres) return;
816
+ if (this.isSqlite) return;
817
+ if (!this.isPostgres && !this.isMysql) return;
740
818
  try {
741
819
  await this.knex.raw("SELECT 1");
742
820
  } catch (e) {
743
- if (e.code === "3D000") {
821
+ if (e.code === "3D000" || e.code === "ER_BAD_DB_ERROR" || e.errno === 1049) {
744
822
  await this.createDatabase();
745
823
  } else {
746
824
  throw e;
@@ -752,18 +830,37 @@ var SqlDriver = class {
752
830
  const connection = config.connection;
753
831
  let dbName = "";
754
832
  const adminConfig = { ...config };
755
- if (typeof connection === "string") {
756
- const url = new URL(connection);
757
- dbName = url.pathname.slice(1);
758
- url.pathname = "/postgres";
759
- adminConfig.connection = url.toString();
833
+ if (this.isPostgres) {
834
+ if (typeof connection === "string") {
835
+ const url = new URL(connection);
836
+ dbName = url.pathname.slice(1);
837
+ url.pathname = "/postgres";
838
+ adminConfig.connection = url.toString();
839
+ } else {
840
+ dbName = connection.database;
841
+ adminConfig.connection = { ...connection, database: "postgres" };
842
+ }
843
+ } else if (this.isMysql) {
844
+ if (typeof connection === "string") {
845
+ const url = new URL(connection);
846
+ dbName = url.pathname.slice(1);
847
+ url.pathname = "/";
848
+ adminConfig.connection = url.toString();
849
+ } else {
850
+ dbName = connection.database;
851
+ const { database: _db, ...rest } = connection;
852
+ adminConfig.connection = rest;
853
+ }
760
854
  } else {
761
- dbName = connection.database;
762
- adminConfig.connection = { ...connection, database: "postgres" };
855
+ return;
763
856
  }
764
857
  const adminKnex = (0, import_knex.default)(adminConfig);
765
858
  try {
766
- await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
859
+ if (this.isPostgres) {
860
+ await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
861
+ } else if (this.isMysql) {
862
+ await adminKnex.raw(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
863
+ }
767
864
  } finally {
768
865
  await adminKnex.destroy();
769
866
  }