@starbemtech/star-db-query-builder 1.3.1 → 1.4.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.
Files changed (76) hide show
  1. package/.claude/skills/star-db-query-builder/SKILL.md +104 -0
  2. package/CHANGELOG.md +107 -55
  3. package/LICENSE +21 -0
  4. package/README.md +92 -1391
  5. package/bin/install-skill.js +53 -0
  6. package/dist/src/core/repository.d.ts +125 -10
  7. package/dist/src/core/repository.js +336 -43
  8. package/dist/src/core/repository.js.map +1 -1
  9. package/dist/src/core/types.d.ts +29 -6
  10. package/dist/src/core/utils.d.ts +119 -0
  11. package/dist/src/core/utils.js +353 -82
  12. package/dist/src/core/utils.js.map +1 -1
  13. package/dist/src/db/IDatabaseClient.d.ts +14 -4
  14. package/dist/src/db/initDb.d.ts +60 -50
  15. package/dist/src/db/initDb.js +96 -64
  16. package/dist/src/db/initDb.js.map +1 -1
  17. package/dist/src/db/mysqlClient.d.ts +3 -8
  18. package/dist/src/db/mysqlClient.js +10 -11
  19. package/dist/src/db/mysqlClient.js.map +1 -1
  20. package/dist/src/db/pgClient.js +1 -2
  21. package/dist/src/db/pgClient.js.map +1 -1
  22. package/dist/src/monitor/monitor.js +7 -0
  23. package/dist/src/monitor/monitor.js.map +1 -1
  24. package/package.json +32 -24
  25. package/.github/workflows/publish.yml +0 -118
  26. package/.prettierignore +0 -3
  27. package/.prettierrc +0 -5
  28. package/ARCHITECTURE.md +0 -313
  29. package/coverage/base.css +0 -224
  30. package/coverage/block-navigation.js +0 -87
  31. package/coverage/favicon.png +0 -0
  32. package/coverage/index.html +0 -131
  33. package/coverage/lcov-report/base.css +0 -224
  34. package/coverage/lcov-report/block-navigation.js +0 -87
  35. package/coverage/lcov-report/favicon.png +0 -0
  36. package/coverage/lcov-report/index.html +0 -131
  37. package/coverage/lcov-report/mysqlClient.ts.html +0 -685
  38. package/coverage/lcov-report/pgClient.ts.html +0 -823
  39. package/coverage/lcov-report/prettify.css +0 -1
  40. package/coverage/lcov-report/prettify.js +0 -2
  41. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  42. package/coverage/lcov-report/sorter.js +0 -210
  43. package/coverage/lcov.info +0 -533
  44. package/coverage/mysqlClient.ts.html +0 -685
  45. package/coverage/pgClient.ts.html +0 -823
  46. package/coverage/prettify.css +0 -1
  47. package/coverage/prettify.js +0 -2
  48. package/coverage/sort-arrow-sprite.png +0 -0
  49. package/coverage/sorter.js +0 -210
  50. package/dist/src/setupTests.d.ts +0 -26
  51. package/dist/src/setupTests.js +0 -43
  52. package/dist/src/setupTests.js.map +0 -1
  53. package/docs/INDEX.md +0 -145
  54. package/docs/methods/findFirst.md +0 -394
  55. package/docs/methods/findMany.md +0 -587
  56. package/docs/methods/insert.md +0 -536
  57. package/docs/methods/insertMany.md +0 -627
  58. package/docs/methods/joins.md +0 -781
  59. package/docs/methods/rawQuery.md +0 -284
  60. package/docs/methods/transactions.md +0 -737
  61. package/eslint.config.mjs +0 -77
  62. package/index.ts +0 -16
  63. package/jest.config.ts +0 -194
  64. package/scripts/release.sh +0 -123
  65. package/src/core/repository.ts +0 -865
  66. package/src/core/types.ts +0 -97
  67. package/src/core/utils.ts +0 -357
  68. package/src/db/IDatabaseClient.ts +0 -16
  69. package/src/db/__tests__/mysqlClient.test.ts +0 -262
  70. package/src/db/__tests__/pgClient.test.ts +0 -260
  71. package/src/db/initDb.ts +0 -181
  72. package/src/db/mysqlClient.ts +0 -200
  73. package/src/db/pgClient.ts +0 -246
  74. package/src/monitor/monitor.ts +0 -16
  75. package/src/setupTests.ts +0 -45
  76. package/tsconfig.test.json +0 -21
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.beginTransaction = exports.withTransaction = exports.rawQuery = exports.joins = exports.deleteMany = exports.deleteOne = exports.updateMany = exports.update = exports.insertMany = exports.insert = exports.findMany = exports.findFirst = void 0;
3
+ exports.beginTransaction = exports.withTransaction = exports.rawQuery = exports.RawQueryError = exports.joins = exports.deleteMany = exports.deleteOne = exports.updateMany = exports.update = exports.upsert = exports.insertMany = exports.insert = exports.findManyCursor = exports.findMany = exports.findFirst = void 0;
4
4
  const uuid_1 = require("uuid");
5
5
  const utils_1 = require("./utils");
6
+ const JOIN_TYPES = new Set(['INNER', 'LEFT', 'RIGHT', 'FULL']);
6
7
  /**
7
8
  * Finds the first record in the specified table
8
9
  *
@@ -21,7 +22,7 @@ const utils_1 = require("./utils");
21
22
  * tableName: 'users',
22
23
  * dbClient: dbClient,
23
24
  * select: ['id', 'name', 'email'],
24
- * where: { status: 'active' },
25
+ * where: { status: { operator: '=', value: 'active' } },
25
26
  * groupBy: ['status'],
26
27
  * orderBy: [{ field: 'created_at', direction: 'DESC' }],
27
28
  * })
@@ -31,7 +32,7 @@ const utils_1 = require("./utils");
31
32
  * tableName: 'users',
32
33
  * dbClient: dbClient,
33
34
  * select: ['id', 'name', 'email'],
34
- * where: { status: 'active' },
35
+ * where: { status: { operator: '=', value: 'active' } },
35
36
  * groupBy: ['status'],
36
37
  * orderBy: [{ field: 'created_at', direction: 'DESC' }],
37
38
  * })
@@ -39,6 +40,7 @@ const utils_1 = require("./utils");
39
40
  const findFirst = async ({ tableName, dbClient, select, where, groupBy, orderBy, }) => {
40
41
  if (!tableName)
41
42
  throw new Error('Table name is required');
43
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
42
44
  if (!dbClient)
43
45
  throw new Error('DB client is required');
44
46
  const fields = (0, utils_1.createSelectFields)(select, dbClient.clientType);
@@ -46,9 +48,10 @@ const findFirst = async ({ tableName, dbClient, select, where, groupBy, orderBy,
46
48
  const orderByClause = (0, utils_1.createOrderByClause)(orderBy);
47
49
  const groupByClause = (0, utils_1.createGroupByClause)(groupBy);
48
50
  const rows = await dbClient.query(`SELECT ${fields} FROM ${tableName}
49
- ${whereClause.length > 7 ? whereClause : ''}
51
+ ${whereClause}
50
52
  ${groupByClause}
51
53
  ${orderByClause}
54
+ LIMIT 1
52
55
  `, params);
53
56
  return rows[0] || null;
54
57
  };
@@ -71,7 +74,7 @@ exports.findFirst = findFirst;
71
74
  * tableName: 'users',
72
75
  * dbClient: dbClient,
73
76
  * select: ['id', 'name', 'email'],
74
- * where: { status: 'active' },
77
+ * where: { status: { operator: '=', value: 'active' } },
75
78
  * groupBy: ['status'],
76
79
  * orderBy: [{ field: 'created_at', direction: 'DESC' }],
77
80
  * limit: 10,
@@ -82,6 +85,7 @@ exports.findFirst = findFirst;
82
85
  const findMany = async ({ tableName, dbClient, select, where, groupBy, orderBy, limit, offset, unaccent, }) => {
83
86
  if (!tableName)
84
87
  throw new Error('Table name is required');
88
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
85
89
  if (!dbClient)
86
90
  throw new Error('DB client is required');
87
91
  const fields = (0, utils_1.createSelectFields)(select, dbClient.clientType);
@@ -91,7 +95,7 @@ const findMany = async ({ tableName, dbClient, select, where, groupBy, orderBy,
91
95
  const limitClause = (0, utils_1.createLimitClause)(limit);
92
96
  const offsetClause = (0, utils_1.createOffsetClause)(offset);
93
97
  const rows = await dbClient.query(`SELECT ${fields} FROM ${tableName}
94
- ${whereClause.length > 7 ? whereClause : ''}
98
+ ${whereClause}
95
99
  ${groupByClause}
96
100
  ${orderByClause}
97
101
  ${limitClause}
@@ -100,6 +104,91 @@ const findMany = async ({ tableName, dbClient, select, where, groupBy, orderBy,
100
104
  return rows || [];
101
105
  };
102
106
  exports.findMany = findMany;
107
+ /**
108
+ * Finds multiple records in the specified table using keyset (cursor)
109
+ * pagination instead of offset/limit
110
+ *
111
+ * Unlike offset-based pagination, keyset pagination stays O(1) regardless of
112
+ * how deep the page is (no `OFFSET N` row-skipping) and does not skip/repeat
113
+ * rows when the underlying data changes between pages. It requires
114
+ * `cursorField` to be a strictly ordered, indexed column — `id` (if
115
+ * sequential) or `created_at` are typical choices; a plain UUID `id` works
116
+ * for uniqueness but its ordering is arbitrary, so prefer a monotonically
117
+ * increasing column when the page order matters to callers.
118
+ *
119
+ * @template T - The type of the records to be returned
120
+ * @param params - Query parameters including table name, database client, select fields, where conditions, cursor field, cursor value, direction, page size and unaccent
121
+ * @returns Promise<CursorPageResult<T>> - The page of records plus the cursor to request the next page (`null` when there is no next page)
122
+ *
123
+ * @throws {Error} When table name is not provided
124
+ * @throws {Error} When database client is not provided
125
+ * @throws {Error} When direction is not ASC or DESC
126
+ *
127
+ * @example
128
+ * // First page
129
+ * const page1 = await findManyCursor({
130
+ * tableName: 'users',
131
+ * dbClient: dbClient,
132
+ * where: { status: { operator: '=', value: 'active' } },
133
+ * cursorField: 'created_at',
134
+ * limit: 20,
135
+ * })
136
+ *
137
+ * @example
138
+ * // Next page
139
+ * const page2 = await findManyCursor({
140
+ * tableName: 'users',
141
+ * dbClient: dbClient,
142
+ * cursorField: 'created_at',
143
+ * cursor: page1.nextCursor,
144
+ * limit: 20,
145
+ * })
146
+ */
147
+ const findManyCursor = async ({ tableName, dbClient, select, where, cursorField = 'id', cursor, direction = 'ASC', limit = 20, unaccent, }) => {
148
+ if (!tableName)
149
+ throw new Error('Table name is required');
150
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
151
+ if (!dbClient)
152
+ throw new Error('DB client is required');
153
+ (0, utils_1.assertValidIdentifier)(cursorField, 'cursor field');
154
+ if (direction !== 'ASC' && direction !== 'DESC') {
155
+ throw new Error(`Invalid direction: "${direction}". Only ASC or DESC are allowed.`);
156
+ }
157
+ if (!Number.isInteger(limit) || limit <= 0) {
158
+ throw new Error(`Invalid limit: "${limit}". Must be a positive integer.`);
159
+ }
160
+ const fields = (0, utils_1.createSelectFields)(select, dbClient.clientType);
161
+ const [whereClause, whereParams, nextIndex] = (0, utils_1.createWhereClause)(where, 1, dbClient.clientType, unaccent);
162
+ const params = [...whereParams];
163
+ let cursorCondition = '';
164
+ if (cursor !== undefined && cursor !== null) {
165
+ const operator = direction === 'ASC' ? '>' : '<';
166
+ const placeholder = dbClient.clientType === 'pg' ? `$${nextIndex}` : '?';
167
+ cursorCondition = `${cursorField} ${operator} ${placeholder}`;
168
+ params.push(cursor);
169
+ }
170
+ let combinedWhere = whereClause;
171
+ if (cursorCondition) {
172
+ combinedWhere = whereClause
173
+ ? `${whereClause} AND ${cursorCondition}`
174
+ : ` WHERE ${cursorCondition}`;
175
+ }
176
+ // fetch one extra row to know whether a next page exists without a
177
+ // separate COUNT query
178
+ const rows = await dbClient.query(`SELECT ${fields} FROM ${tableName}
179
+ ${combinedWhere}
180
+ ORDER BY ${cursorField} ${direction}
181
+ LIMIT ${limit + 1}
182
+ `, params);
183
+ const hasMore = rows.length > limit;
184
+ const data = hasMore ? rows.slice(0, limit) : rows;
185
+ const lastRow = data[data.length - 1];
186
+ return {
187
+ data,
188
+ nextCursor: hasMore && lastRow ? (lastRow[cursorField] ?? null) : null,
189
+ };
190
+ };
191
+ exports.findManyCursor = findManyCursor;
103
192
  /**
104
193
  * Inserts a new record into the specified table
105
194
  *
@@ -126,18 +215,20 @@ exports.findMany = findMany;
126
215
  const insert = async ({ tableName, dbClient, data, returning, }) => {
127
216
  if (!tableName)
128
217
  throw new Error('Table name is required');
218
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
129
219
  if (!dbClient)
130
220
  throw new Error('DB client is required');
131
221
  if (!data)
132
222
  throw new Error('Data object is required');
133
- const keys = dbClient.clientType === 'pg'
134
- ? Object.keys(data).map((key) => key === 'authorization' ? `"${key}"` : key)
135
- : Object.keys(data);
223
+ const rawKeys = Object.keys(data);
224
+ rawKeys.forEach((key) => (0, utils_1.assertValidIdentifier)(key, 'column name'));
225
+ (0, utils_1.assertNoAutoManagedColumns)(rawKeys, 'insert');
226
+ const keys = rawKeys.map((key) => (0, utils_1.quoteIdentifier)(key, dbClient.clientType));
136
227
  const values = Object.values(data);
137
- keys.unshift('id');
228
+ keys.unshift((0, utils_1.quoteIdentifier)('id', dbClient.clientType));
138
229
  const generatedUUID = (0, uuid_1.v4)();
139
230
  values.unshift(generatedUUID);
140
- keys.push('updated_at');
231
+ keys.push((0, utils_1.quoteIdentifier)('updated_at', dbClient.clientType));
141
232
  values.push(new Date());
142
233
  const placeholders = (0, utils_1.generatePlaceholders)(keys, dbClient.clientType);
143
234
  let query = `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES (${placeholders})`;
@@ -185,21 +276,41 @@ exports.insert = insert;
185
276
  const insertMany = async ({ tableName, dbClient, data, returning, }) => {
186
277
  if (!tableName)
187
278
  throw new Error('Table name is required');
279
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
188
280
  if (!dbClient)
189
281
  throw new Error('DB client is required');
190
282
  if (!data || data.length === 0)
191
283
  throw new Error('Data array is required and cannot be empty');
192
284
  const firstItem = data[0];
193
- const keys = dbClient.clientType === 'pg'
194
- ? Object.keys(firstItem).map((key) => key === 'authorization' ? `"${key}"` : key)
195
- : Object.keys(firstItem);
196
- const allKeys = ['id', ...keys, 'updated_at'];
285
+ const rawKeys = Object.keys(firstItem);
286
+ rawKeys.forEach((key) => (0, utils_1.assertValidIdentifier)(key, 'column name'));
287
+ (0, utils_1.assertNoAutoManagedColumns)(rawKeys, 'insertMany');
288
+ const rawKeySet = new Set(rawKeys);
289
+ data.forEach((item, index) => {
290
+ const itemKeys = Object.keys(item);
291
+ const itemKeySet = new Set(itemKeys);
292
+ const missing = rawKeys.filter((key) => !itemKeySet.has(key));
293
+ const extra = itemKeys.filter((key) => !rawKeySet.has(key));
294
+ if (missing.length > 0 || extra.length > 0) {
295
+ throw new Error(`insertMany: item at index ${index} has different keys than the first item.` +
296
+ (missing.length > 0 ? ` Missing: [${missing.join(', ')}].` : '') +
297
+ (extra.length > 0 ? ` Unexpected: [${extra.join(', ')}].` : ''));
298
+ }
299
+ });
300
+ const keys = rawKeys.map((key) => (0, utils_1.quoteIdentifier)(key, dbClient.clientType));
301
+ const allKeys = [
302
+ (0, utils_1.quoteIdentifier)('id', dbClient.clientType),
303
+ ...keys,
304
+ (0, utils_1.quoteIdentifier)('updated_at', dbClient.clientType),
305
+ ];
306
+ (0, utils_1.assertWithinBindParamLimit)(data.length * allKeys.length, 'insertMany');
197
307
  let query = `INSERT INTO ${tableName} (${allKeys.join(', ')}) VALUES `;
198
308
  const allValues = [];
199
309
  const valueRows = [];
200
310
  const generatedIds = [];
201
311
  data.forEach((item, rowIndex) => {
202
- const values = Object.values(item);
312
+ const record = item;
313
+ const values = rawKeys.map((key) => record[key]);
203
314
  const generatedUUID = (0, uuid_1.v4)();
204
315
  generatedIds.push(generatedUUID);
205
316
  const currentValues = [generatedUUID, ...values, new Date()];
@@ -237,6 +348,116 @@ const insertMany = async ({ tableName, dbClient, data, returning, }) => {
237
348
  return inserted;
238
349
  };
239
350
  exports.insertMany = insertMany;
351
+ /**
352
+ * Inserts a record, or updates it in place if it collides with an existing
353
+ * unique/primary key
354
+ *
355
+ * Builds `INSERT ... ON CONFLICT (...) DO UPDATE SET ...` for pg and
356
+ * `INSERT ... ON DUPLICATE KEY UPDATE ...` for mysql. `conflictFields` must
357
+ * name columns actually covered by a unique or primary key constraint on
358
+ * `tableName` — this function does not create or verify that constraint, it
359
+ * only builds SQL that assumes it exists. For mysql, `conflictFields` is not
360
+ * part of the generated SQL (`ON DUPLICATE KEY UPDATE` relies on the table's
361
+ * own constraint) — it is used only to re-select the row afterwards, since
362
+ * mysql has no `RETURNING`.
363
+ *
364
+ * @template P - The type of the data to be upserted
365
+ * @template R - The type of the record to be returned
366
+ * @param params - Query parameters including table name, database client, data, conflict target fields, optional fields to update on conflict (defaults to every field in data), and optional returning fields
367
+ * @returns Promise<R> - The inserted or updated record
368
+ *
369
+ * @throws {Error} When table name is not provided
370
+ * @throws {Error} When database client is not provided
371
+ * @throws {Error} When data object is not provided
372
+ * @throws {Error} When conflictFields is not provided or empty
373
+ *
374
+ * @example
375
+ * const record = await upsert({
376
+ * tableName: 'users',
377
+ * dbClient: dbClient,
378
+ * data: { email: 'john.doe@example.com', name: 'John Doe' },
379
+ * conflictFields: ['email'],
380
+ * returning: ['id', 'name', 'email'],
381
+ * })
382
+ */
383
+ const upsert = async ({ tableName, dbClient, data, conflictFields, updateFields, returning, }) => {
384
+ if (!tableName)
385
+ throw new Error('Table name is required');
386
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
387
+ if (!dbClient)
388
+ throw new Error('DB client is required');
389
+ if (!data)
390
+ throw new Error('Data object is required');
391
+ if (!conflictFields || conflictFields.length === 0)
392
+ throw new Error('conflictFields is required and cannot be empty');
393
+ conflictFields.forEach((field) => (0, utils_1.assertValidIdentifier)(field, 'conflict field'));
394
+ const rawKeys = Object.keys(data);
395
+ rawKeys.forEach((key) => (0, utils_1.assertValidIdentifier)(key, 'column name'));
396
+ (0, utils_1.assertNoAutoManagedColumns)(rawKeys, 'upsert');
397
+ const rawKeySet = new Set(rawKeys);
398
+ const fieldsToUpdate = updateFields && updateFields.length > 0 ? updateFields : rawKeys;
399
+ fieldsToUpdate.forEach((key) => (0, utils_1.assertValidIdentifier)(key, 'column name'));
400
+ // 'updated_at' is always refreshed on conflict regardless of `data`
401
+ // (the function injects it below), so naming it in `updateFields` is
402
+ // valid even though it's never a key of `data`.
403
+ const unknownUpdateFields = fieldsToUpdate.filter((key) => key !== 'updated_at' && !rawKeySet.has(key));
404
+ if (unknownUpdateFields.length > 0) {
405
+ throw new Error(`upsert: updateFields references column(s) not present in data: [${unknownUpdateFields.join(', ')}]. Only columns included in data can be refreshed on conflict — a column left out of data would resolve to its table default instead of the intended value.`);
406
+ }
407
+ if (dbClient.clientType === 'mysql') {
408
+ const record = data;
409
+ const missingConflictValues = conflictFields.filter((field) => record[field] === undefined);
410
+ if (missingConflictValues.length > 0) {
411
+ throw new Error(`upsert: conflictFields references column(s) not present in data: [${missingConflictValues.join(', ')}]. mysql has no RETURNING, so upsert() re-selects the row by these columns after the write — they must be included in data.`);
412
+ }
413
+ }
414
+ const keys = rawKeys.map((key) => (0, utils_1.quoteIdentifier)(key, dbClient.clientType));
415
+ const values = Object.values(data);
416
+ keys.unshift((0, utils_1.quoteIdentifier)('id', dbClient.clientType));
417
+ const generatedUUID = (0, uuid_1.v4)();
418
+ values.unshift(generatedUUID);
419
+ keys.push((0, utils_1.quoteIdentifier)('updated_at', dbClient.clientType));
420
+ values.push(new Date());
421
+ const placeholders = (0, utils_1.generatePlaceholders)(keys, dbClient.clientType);
422
+ let query = `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES (${placeholders})`;
423
+ if (dbClient.clientType === 'pg') {
424
+ const conflictTarget = conflictFields
425
+ .map((field) => (0, utils_1.quoteIdentifier)(field, 'pg'))
426
+ .join(', ');
427
+ const updateSet = Array.from(new Set([...fieldsToUpdate, 'updated_at']))
428
+ .map((key) => {
429
+ const quoted = (0, utils_1.quoteIdentifier)(key, 'pg');
430
+ return `${quoted} = EXCLUDED.${quoted}`;
431
+ })
432
+ .join(', ');
433
+ query += ` ON CONFLICT (${conflictTarget}) DO UPDATE SET ${updateSet}`;
434
+ if (returning && returning.length > 0) {
435
+ query += ` RETURNING ${(0, utils_1.createSelectFields)(returning, dbClient.clientType)}`;
436
+ }
437
+ const result = await dbClient.query(query, values);
438
+ return result[0];
439
+ }
440
+ const updateSet = Array.from(new Set([...fieldsToUpdate, 'updated_at']))
441
+ .map((key) => {
442
+ const quoted = (0, utils_1.quoteIdentifier)(key, 'mysql');
443
+ return `${quoted} = VALUES(${quoted})`;
444
+ })
445
+ .join(', ');
446
+ query += ` ON DUPLICATE KEY UPDATE ${updateSet}`;
447
+ await dbClient.query(query, values);
448
+ const record = data;
449
+ const whereClause = conflictFields
450
+ .map((field) => `${(0, utils_1.quoteIdentifier)(field, 'mysql')} = ?`)
451
+ .join(' AND ');
452
+ const conflictValues = conflictFields.map((field) => record[field]);
453
+ const rows = await dbClient.query(`SELECT ${returning && returning.length > 0
454
+ ? (0, utils_1.createSelectFields)(returning, dbClient.clientType)
455
+ : '*'} FROM ${tableName}
456
+ WHERE ${whereClause}
457
+ `, conflictValues);
458
+ return rows[0];
459
+ };
460
+ exports.upsert = upsert;
240
461
  /**
241
462
  * Updates a record in the specified table
242
463
  *
@@ -264,16 +485,24 @@ exports.insertMany = insertMany;
264
485
  const update = async ({ tableName, dbClient, id, data, returning, }) => {
265
486
  if (!tableName)
266
487
  throw new Error('Table name is required');
488
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
267
489
  if (!dbClient)
268
490
  throw new Error('DB client is required');
269
491
  if (!id)
270
- throw new Error('ID is required');
492
+ throw new Error(`ID is required for update() on table "${tableName}"`);
271
493
  if (!data)
272
494
  throw new Error('Data object is required');
273
- const keys = Object.keys(data);
274
- const values = Object.values(data);
275
- const setClause = (0, utils_1.generateSetClause)(keys, dbClient.clientType);
276
- let query = `UPDATE ${tableName} SET ${setClause} WHERE id = '${id}'`;
495
+ const definedData = (0, utils_1.stripUndefinedValues)(data);
496
+ if (Object.keys(definedData).length === 0)
497
+ throw new Error('Data object must have at least one field to update');
498
+ const keys = Object.keys(definedData);
499
+ keys.forEach((key) => (0, utils_1.assertValidIdentifier)(key, 'column name'));
500
+ const values = Object.values(definedData);
501
+ const quotedKeys = keys.map((key) => (0, utils_1.quoteIdentifier)(key, dbClient.clientType));
502
+ const setClause = (0, utils_1.generateSetClause)(quotedKeys, dbClient.clientType);
503
+ const idPlaceholder = dbClient.clientType === 'pg' ? `$${values.length + 1}` : '?';
504
+ let query = `UPDATE ${tableName} SET ${setClause} WHERE id = ${idPlaceholder}`;
505
+ values.push(id);
277
506
  if (dbClient.clientType === 'pg') {
278
507
  if (returning && returning.length > 0) {
279
508
  query += ` RETURNING ${(0, utils_1.createSelectFields)(returning, dbClient.clientType)}`;
@@ -313,45 +542,62 @@ exports.update = update;
313
542
  * tableName: 'users',
314
543
  * dbClient: dbClient,
315
544
  * data: { name: 'John Doe', email: 'john.doe@example.com' },
316
- * where: { status: 'active' },
545
+ * where: { status: { operator: '=', value: 'active' } },
317
546
  * returning: ['id', 'name', 'email'],
318
547
  * })
319
548
  */
320
549
  const updateMany = async ({ tableName, dbClient, data, where, returning, }) => {
321
550
  if (!tableName)
322
551
  throw new Error('Table name is required');
552
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
323
553
  if (!dbClient)
324
554
  throw new Error('DB client is required');
325
555
  if (!data)
326
556
  throw new Error('Data object is required');
557
+ const definedData = (0, utils_1.stripUndefinedValues)(data);
558
+ if (Object.keys(definedData).length === 0)
559
+ throw new Error('Data object must have at least one field to update');
327
560
  if (!where)
328
561
  throw new Error('Where condition is required');
329
- const keys = Object.keys(data);
330
- const values = Object.values(data);
562
+ const keys = Object.keys(definedData);
563
+ keys.forEach((key) => (0, utils_1.assertValidIdentifier)(key, 'column name'));
564
+ const values = Object.values(definedData);
331
565
  // Generate SET clause with correct placeholders
332
- const setClause = keys
333
- .map((key, index) => dbClient.clientType === 'pg' ? `${key} = $${index + 1}` : `${key} = ?`)
334
- .join(', ');
566
+ const quotedKeys = keys.map((key) => (0, utils_1.quoteIdentifier)(key, dbClient.clientType));
567
+ const setClause = (0, utils_1.generateSetClause)(quotedKeys, dbClient.clientType);
335
568
  // Generate WHERE clause with placeholders starting after SET values
336
569
  const [whereClause, whereParams] = (0, utils_1.createWhereClause)(where, values.length + 1, dbClient.clientType);
337
- let query = `UPDATE ${tableName} SET ${setClause} ${whereClause}`;
570
+ if (!whereClause) {
571
+ throw new Error('updateMany: where condition produced an empty WHERE clause — refusing to update every row in the table. Pass at least one condition.');
572
+ }
573
+ let query = `UPDATE ${tableName} SET ${setClause}${whereClause}`;
338
574
  if (dbClient.clientType === 'pg') {
339
575
  if (returning && returning.length > 0) {
340
576
  query += ` RETURNING ${(0, utils_1.createSelectFields)(returning, dbClient.clientType)}`;
341
577
  }
578
+ return await dbClient.query(query, [...values, ...whereParams]);
342
579
  }
343
- const updated = await dbClient.query(query, [...values, ...whereParams]);
344
- if (dbClient.clientType === 'mysql') {
345
- // For MySQL, we need to fetch the updated records separately
346
- // since MySQL doesn't support RETURNING clause
347
- const rows = await dbClient.query(`SELECT ${returning && returning.length > 0
348
- ? (0, utils_1.createSelectFields)(returning, dbClient.clientType)
349
- : '*'} FROM ${tableName}
350
- ${whereClause}
351
- `, whereParams);
352
- return rows;
580
+ // MySQL has no RETURNING, so the rows have to be fetched with a separate
581
+ // SELECT. That SELECT must capture *which* rows matched before the UPDATE
582
+ // runs: re-running the original WHERE afterwards (as this used to do)
583
+ // silently returns the wrong rows whenever the UPDATE changes a column the
584
+ // WHERE itself filters on — e.g. `where: { status: 'pending' }` with
585
+ // `data: { status: 'done' }` matches zero rows once every row's status has
586
+ // already flipped to 'done', so the caller got back `[]` instead of the
587
+ // rows it just updated.
588
+ const idColumn = (0, utils_1.quoteIdentifier)('id', dbClient.clientType);
589
+ const matchedIdRows = await dbClient.query(`SELECT ${idColumn} FROM ${tableName}${whereClause}`, whereParams);
590
+ if (matchedIdRows.length === 0) {
591
+ await dbClient.query(query, [...values, ...whereParams]);
592
+ return [];
353
593
  }
354
- return updated;
594
+ const ids = matchedIdRows.map((row) => row.id);
595
+ await dbClient.query(query, [...values, ...whereParams]);
596
+ const idPlaceholders = ids.map(() => '?').join(', ');
597
+ const rows = await dbClient.query(`SELECT ${returning && returning.length > 0
598
+ ? (0, utils_1.createSelectFields)(returning, dbClient.clientType)
599
+ : '*'} FROM ${tableName} WHERE ${idColumn} IN (${idPlaceholders})`, ids);
600
+ return rows;
355
601
  };
356
602
  exports.updateMany = updateMany;
357
603
  /**
@@ -379,10 +625,11 @@ exports.updateMany = updateMany;
379
625
  const deleteOne = async ({ tableName, dbClient, id, permanently = false, }) => {
380
626
  if (!tableName)
381
627
  throw new Error('Table name is required');
628
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
382
629
  if (!dbClient)
383
630
  throw new Error('DB client is required');
384
631
  if (!id)
385
- throw new Error('ID is required');
632
+ throw new Error(`ID is required for deleteOne() on table "${tableName}"`);
386
633
  await dbClient.query(permanently
387
634
  ? `DELETE FROM ${tableName} WHERE id = ${dbClient.clientType === 'pg' ? '$1' : '?'}`
388
635
  : `UPDATE ${tableName} SET status = 'deleted' WHERE id = ${dbClient.clientType === 'pg' ? '$1' : '?'}`, [id]);
@@ -415,12 +662,15 @@ exports.deleteOne = deleteOne;
415
662
  const deleteMany = async ({ tableName, dbClient, ids, field = 'id', permanently = false, }) => {
416
663
  if (!tableName)
417
664
  throw new Error('Table name is required');
665
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
418
666
  if (!dbClient)
419
667
  throw new Error('DB client is required');
420
668
  if (!ids || ids.length === 0)
421
669
  throw new Error('IDs are required and cannot be empty');
422
670
  if (!field)
423
671
  throw new Error('Field is required');
672
+ (0, utils_1.assertValidIdentifier)(field, 'field');
673
+ (0, utils_1.assertWithinBindParamLimit)(ids.length, 'deleteMany');
424
674
  const placeholders = dbClient.clientType === 'pg'
425
675
  ? ids.map((_, index) => `$${index + 1}`).join(', ')
426
676
  : ids.map(() => '?').join(', ');
@@ -451,7 +701,7 @@ exports.deleteMany = deleteMany;
451
701
  * dbClient: dbClient,
452
702
  * select: ['id', 'name', 'email'],
453
703
  * joins: [{ type: 'INNER', table: 'orders', on: 'users.id = orders.user_id' }],
454
- * where: { status: 'active' },
704
+ * where: { status: { operator: '=', value: 'active' } },
455
705
  * groupBy: ['status'],
456
706
  * orderBy: [{ field: 'created_at', direction: 'DESC' }],
457
707
  * limit: 10,
@@ -462,8 +712,18 @@ exports.deleteMany = deleteMany;
462
712
  const joins = async ({ tableName, dbClient, select, joins, where, groupBy, orderBy, limit, offset, unaccent, }) => {
463
713
  if (!tableName)
464
714
  throw new Error('Table name is required');
715
+ (0, utils_1.assertValidIdentifier)(tableName, 'table name');
465
716
  if (!dbClient)
466
717
  throw new Error('DB client is required');
718
+ if (joins) {
719
+ joins.forEach((join) => {
720
+ if (!JOIN_TYPES.has(join.type)) {
721
+ throw new Error(`Invalid join type: "${join.type}". Only INNER, LEFT, RIGHT or FULL are allowed.`);
722
+ }
723
+ (0, utils_1.assertValidIdentifier)(join.table, 'join table');
724
+ (0, utils_1.assertSafeSqlFragment)(join.on, 'join on condition');
725
+ });
726
+ }
467
727
  const fields = Array.isArray(select) ? select : [];
468
728
  const selectFields = (0, utils_1.createSelectFields)(fields, dbClient.clientType);
469
729
  const [whereClause, params] = (0, utils_1.createWhereClause)(where, 1, dbClient.clientType, unaccent);
@@ -486,6 +746,39 @@ const joins = async ({ tableName, dbClient, select, joins, where, groupBy, order
486
746
  return rows;
487
747
  };
488
748
  exports.joins = joins;
749
+ /**
750
+ * Error thrown by `rawQuery` when the underlying driver query fails.
751
+ *
752
+ * Carries over the original driver error as `cause` plus the common
753
+ * cross-driver fields consumers actually branch on (e.g. Postgres'
754
+ * `code: '23505'` for a unique violation, `detail`, `constraint`; mysql2's
755
+ * `errno`/`sqlState`) — `rawQuery` used to wrap failures in a plain `Error`
756
+ * built from just `error.message`, which discarded all of this and left
757
+ * callers with no way to distinguish a conflict from a syntax error.
758
+ */
759
+ class RawQueryError extends Error {
760
+ constructor(message, cause) {
761
+ super(message);
762
+ this.name = 'RawQueryError';
763
+ // Set manually rather than via the ES2022 `Error(message, { cause })`
764
+ // constructor form, which this project's `lib` target doesn't include.
765
+ this.cause = cause;
766
+ if (cause && typeof cause === 'object') {
767
+ const driverError = cause;
768
+ if (typeof driverError.code === 'string')
769
+ this.code = driverError.code;
770
+ if (typeof driverError.detail === 'string')
771
+ this.detail = driverError.detail;
772
+ if (typeof driverError.constraint === 'string')
773
+ this.constraint = driverError.constraint;
774
+ if (typeof driverError.errno === 'number')
775
+ this.errno = driverError.errno;
776
+ if (typeof driverError.sqlState === 'string')
777
+ this.sqlState = driverError.sqlState;
778
+ }
779
+ }
780
+ }
781
+ exports.RawQueryError = RawQueryError;
489
782
  /**
490
783
  * Executa uma query SQL raw diretamente no banco de dados
491
784
  * @param params - Parâmetros da query raw
@@ -531,7 +824,7 @@ const rawQuery = async ({ dbClient, sql, params = [], }) => {
531
824
  return result;
532
825
  }
533
826
  catch (error) {
534
- throw new Error(`Raw query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
827
+ throw new RawQueryError(`Raw query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
535
828
  }
536
829
  };
537
830
  exports.rawQuery = rawQuery;
@@ -548,7 +841,7 @@ exports.rawQuery = rawQuery;
548
841
  * select: ['id', 'name', 'email'],
549
842
  * from: 'users',
550
843
  * joins: [{ type: 'INNER', table: 'orders', on: 'users.id = orders.user_id' }],
551
- * where: { status: 'active' },
844
+ * where: { status: { operator: '=', value: 'active' } },
552
845
  * groupBy: ['status'],
553
846
  * orderBy: [{ field: 'created_at', direction: 'DESC' }],
554
847
  * limit: 10,