@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.
- package/.claude/skills/star-db-query-builder/SKILL.md +104 -0
- package/CHANGELOG.md +107 -55
- package/LICENSE +21 -0
- package/README.md +92 -1391
- package/bin/install-skill.js +53 -0
- package/dist/src/core/repository.d.ts +125 -10
- package/dist/src/core/repository.js +336 -43
- package/dist/src/core/repository.js.map +1 -1
- package/dist/src/core/types.d.ts +29 -6
- package/dist/src/core/utils.d.ts +119 -0
- package/dist/src/core/utils.js +353 -82
- package/dist/src/core/utils.js.map +1 -1
- package/dist/src/db/IDatabaseClient.d.ts +14 -4
- package/dist/src/db/initDb.d.ts +60 -50
- package/dist/src/db/initDb.js +96 -64
- package/dist/src/db/initDb.js.map +1 -1
- package/dist/src/db/mysqlClient.d.ts +3 -8
- package/dist/src/db/mysqlClient.js +10 -11
- package/dist/src/db/mysqlClient.js.map +1 -1
- package/dist/src/db/pgClient.js +1 -2
- package/dist/src/db/pgClient.js.map +1 -1
- package/dist/src/monitor/monitor.js +7 -0
- package/dist/src/monitor/monitor.js.map +1 -1
- package/package.json +32 -24
- package/.github/workflows/publish.yml +0 -118
- package/.prettierignore +0 -3
- package/.prettierrc +0 -5
- package/ARCHITECTURE.md +0 -313
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -131
- package/coverage/lcov-report/mysqlClient.ts.html +0 -685
- package/coverage/lcov-report/pgClient.ts.html +0 -823
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov.info +0 -533
- package/coverage/mysqlClient.ts.html +0 -685
- package/coverage/pgClient.ts.html +0 -823
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/dist/src/setupTests.d.ts +0 -26
- package/dist/src/setupTests.js +0 -43
- package/dist/src/setupTests.js.map +0 -1
- package/docs/INDEX.md +0 -145
- package/docs/methods/findFirst.md +0 -394
- package/docs/methods/findMany.md +0 -587
- package/docs/methods/insert.md +0 -536
- package/docs/methods/insertMany.md +0 -627
- package/docs/methods/joins.md +0 -781
- package/docs/methods/rawQuery.md +0 -284
- package/docs/methods/transactions.md +0 -737
- package/eslint.config.mjs +0 -77
- package/index.ts +0 -16
- package/jest.config.ts +0 -194
- package/scripts/release.sh +0 -123
- package/src/core/repository.ts +0 -865
- package/src/core/types.ts +0 -97
- package/src/core/utils.ts +0 -357
- package/src/db/IDatabaseClient.ts +0 -16
- package/src/db/__tests__/mysqlClient.test.ts +0 -262
- package/src/db/__tests__/pgClient.test.ts +0 -260
- package/src/db/initDb.ts +0 -181
- package/src/db/mysqlClient.ts +0 -200
- package/src/db/pgClient.ts +0 -246
- package/src/monitor/monitor.ts +0 -16
- package/src/setupTests.ts +0 -45
- 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
|
|
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
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
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
|
|
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(
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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(
|
|
330
|
-
|
|
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
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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,
|