@objectstack/driver-sql 3.2.9 → 3.3.1

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