@starbemtech/star-db-query-builder 1.3.1 → 1.4.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/.claude/skills/star-db-query-builder/SKILL.md +104 -0
- package/CHANGELOG.md +82 -45
- package/LICENSE +21 -0
- package/README.md +194 -94
- package/bin/install-skill.js +53 -0
- package/dist/src/core/repository.d.ts +92 -9
- package/dist/src/core/repository.js +275 -27
- package/dist/src/core/repository.js.map +1 -1
- package/dist/src/core/types.d.ts +16 -1
- package/dist/src/core/utils.d.ts +102 -0
- package/dist/src/core/utils.js +287 -70
- package/dist/src/core/utils.js.map +1 -1
- 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 +9 -11
- package/dist/src/db/mysqlClient.js.map +1 -1
- package/dist/src/db/pgClient.js +0 -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 +27 -19
- 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
package/README.md
CHANGED
|
@@ -11,8 +11,10 @@ A powerful and flexible database query builder library for Node.js applications,
|
|
|
11
11
|
- [Query Methods](#query-methods)
|
|
12
12
|
- [findFirst](#findfirst)
|
|
13
13
|
- [findMany](#findmany)
|
|
14
|
+
- [findManyCursor](#findmanycursor)
|
|
14
15
|
- [insert](#insert)
|
|
15
16
|
- [insertMany](#insertmany)
|
|
17
|
+
- [upsert](#upsert)
|
|
16
18
|
- [update](#update)
|
|
17
19
|
- [updateMany](#updatemany)
|
|
18
20
|
- [deleteOne](#deleteone)
|
|
@@ -30,6 +32,16 @@ A powerful and flexible database query builder library for Node.js applications,
|
|
|
30
32
|
- [Contributing](#contributing)
|
|
31
33
|
- [License](#license)
|
|
32
34
|
|
|
35
|
+
## 📚 Full Documentation
|
|
36
|
+
|
|
37
|
+
This README covers everything at a glance. For the deep-dive version of each method (parameters, generated SQL for pg/mysql, edge cases, error messages), see [`docs/INDEX.md`](docs/INDEX.md) or jump straight to a method:
|
|
38
|
+
|
|
39
|
+
- [findFirst](docs/methods/findFirst.md) · [findMany](docs/methods/findMany.md) · [findManyCursor](docs/methods/findManyCursor.md)
|
|
40
|
+
- [insert](docs/methods/insert.md) · [insertMany](docs/methods/insertMany.md) · [upsert](docs/methods/upsert.md)
|
|
41
|
+
- [joins](docs/methods/joins.md) · [rawQuery](docs/methods/rawQuery.md) · [transactions](docs/methods/transactions.md)
|
|
42
|
+
|
|
43
|
+
`update`, `updateMany`, `deleteOne`, `deleteMany`, `initDb`, `getDbClient` have no dedicated file yet in `docs/methods/` — this README and the JSDoc above each function in `src/core/repository.ts` are the reference for those until one exists.
|
|
44
|
+
|
|
33
45
|
## ✨ Features
|
|
34
46
|
|
|
35
47
|
### 🔧 **Core Functionality**
|
|
@@ -66,6 +78,16 @@ pnpm add @starbemtech/star-db-query-builder
|
|
|
66
78
|
yarn add @starbemtech/star-db-query-builder
|
|
67
79
|
```
|
|
68
80
|
|
|
81
|
+
### 🤖 AI agent skill (Claude Code)
|
|
82
|
+
|
|
83
|
+
If your team uses Claude Code, install the bundled skill so agents get correct usage guidance (method signatures, gotchas like the `update()` operator shape, `upsert()` mysql constraint requirement, etc.) instead of guessing:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx star-db-query-builder-install-skill
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run it from your repo's root, after installing this package. It copies the skill into `.claude/skills/star-db-query-builder/SKILL.md` in your repo — commit that file so the rest of the team gets it too. Safe to re-run after upgrading the package; it re-syncs from whatever version is currently installed.
|
|
90
|
+
|
|
69
91
|
## Quick Start
|
|
70
92
|
|
|
71
93
|
```typescript
|
|
@@ -118,10 +140,13 @@ await initDb({
|
|
|
118
140
|
type: 'pg' | 'mysql', // Database type
|
|
119
141
|
options: PoolConfig | MySqlPoolOptions, // Connection options
|
|
120
142
|
retryOptions?: RetryOptions, // Optional retry configuration
|
|
121
|
-
installUnaccentExtension?: boolean // PostgreSQL unaccent extension
|
|
143
|
+
installUnaccentExtension?: boolean, // PostgreSQL unaccent extension
|
|
144
|
+
queryTimeout?: number // Optional query timeout in ms (see below)
|
|
122
145
|
})
|
|
123
146
|
```
|
|
124
147
|
|
|
148
|
+
`queryTimeout` is applied to every query run through this client. For PostgreSQL it maps onto the pool's `query_timeout` (an explicit `query_timeout` already set in `options` takes precedence over it). For MySQL, since `mysql2` has no pool-wide query timeout, it is passed as the `timeout` option on every individual query.
|
|
149
|
+
|
|
125
150
|
#### PostgreSQL Example
|
|
126
151
|
|
|
127
152
|
```typescript
|
|
@@ -187,10 +212,50 @@ const defaultClient = getDbClient()
|
|
|
187
212
|
const analyticsClient = getDbClient('analytics')
|
|
188
213
|
```
|
|
189
214
|
|
|
215
|
+
### getAllDbClients
|
|
216
|
+
|
|
217
|
+
Retrieves every registered database client, keyed by the name it was registered under (including `'default'`).
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const clients = getAllDbClients() // Record<string, IDatabaseClient>
|
|
221
|
+
const names = Object.keys(clients) // ['default', 'analytics', ...]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### closeDb
|
|
225
|
+
|
|
226
|
+
Closes a database client's connection pool and removes it from the registry. Use this to release connections gracefully on application shutdown or between tests — `initDb` does not release the pools it creates on its own.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
await closeDb() // closes the default client
|
|
230
|
+
await closeDb('analytics') // closes a named client
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Throws if the named client is not initialized.
|
|
234
|
+
|
|
235
|
+
### closeAllDbClients
|
|
236
|
+
|
|
237
|
+
Closes every registered database client's connection pool.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
await closeAllDbClients()
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### resetDbClients
|
|
244
|
+
|
|
245
|
+
Clears the in-memory client/pool registry **without** closing any connection — a synchronous escape hatch for test suites and hot-reload tooling that need a clean registry between runs (e.g. calling `initDb` again with the same name without first awaiting a real `closeDb`). Any real, non-mocked pool left registered is orphaned, not released. Production code that wants to release connections should use `closeDb`/`closeAllDbClients` instead.
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
afterEach(() => {
|
|
249
|
+
resetDbClients() // test isolation only — pools here are mocked
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
190
253
|
## Query Methods
|
|
191
254
|
|
|
192
255
|
### findFirst
|
|
193
256
|
|
|
257
|
+
📖 [Full docs](docs/methods/findFirst.md)
|
|
258
|
+
|
|
194
259
|
Finds the first record that matches the specified conditions.
|
|
195
260
|
|
|
196
261
|
```typescript
|
|
@@ -248,6 +313,8 @@ const latestUser = await findFirst({
|
|
|
248
313
|
|
|
249
314
|
### findMany
|
|
250
315
|
|
|
316
|
+
📖 [Full docs](docs/methods/findMany.md)
|
|
317
|
+
|
|
251
318
|
Finds multiple records that match the specified conditions.
|
|
252
319
|
|
|
253
320
|
```typescript
|
|
@@ -310,8 +377,56 @@ const userStats = await findMany({
|
|
|
310
377
|
})
|
|
311
378
|
```
|
|
312
379
|
|
|
380
|
+
### findManyCursor
|
|
381
|
+
|
|
382
|
+
📖 [Full docs](docs/methods/findManyCursor.md)
|
|
383
|
+
|
|
384
|
+
Finds multiple records using keyset (cursor) pagination instead of offset/limit — cost stays flat regardless of page depth, and pages don't skip/repeat rows when data changes between calls.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
const page = await findManyCursor<T>({
|
|
388
|
+
tableName: string,
|
|
389
|
+
dbClient: IDatabaseClient,
|
|
390
|
+
select?: string[],
|
|
391
|
+
where?: Conditions<T>,
|
|
392
|
+
cursorField?: string, // default: 'id'
|
|
393
|
+
cursor?: string | number, // omit for the first page
|
|
394
|
+
direction?: 'ASC' | 'DESC',// default: 'ASC'
|
|
395
|
+
limit?: number, // default: 20
|
|
396
|
+
unaccent?: boolean
|
|
397
|
+
}): Promise<{ data: T[]; nextCursor: string | number | null }>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
#### Examples
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// First page
|
|
404
|
+
const page1 = await findManyCursor({
|
|
405
|
+
tableName: 'users',
|
|
406
|
+
dbClient,
|
|
407
|
+
where: { status: { operator: '=', value: 'active' } },
|
|
408
|
+
cursorField: 'created_at',
|
|
409
|
+
limit: 20,
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
// Next page
|
|
413
|
+
const page2 = await findManyCursor({
|
|
414
|
+
tableName: 'users',
|
|
415
|
+
dbClient,
|
|
416
|
+
cursorField: 'created_at',
|
|
417
|
+
cursor: page1.nextCursor,
|
|
418
|
+
limit: 20,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// page.nextCursor is null once there are no more rows past this page
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
`findManyCursor` is a separate function, not an option on `findMany` — its return shape (`{ data, nextCursor }`) differs from `findMany`'s plain `T[]`.
|
|
425
|
+
|
|
313
426
|
### insert
|
|
314
427
|
|
|
428
|
+
📖 [Full docs](docs/methods/insert.md)
|
|
429
|
+
|
|
315
430
|
Inserts a single record into the database.
|
|
316
431
|
|
|
317
432
|
```typescript
|
|
@@ -377,6 +492,8 @@ const user: User = await insert<UserData, User>({
|
|
|
377
492
|
|
|
378
493
|
### insertMany
|
|
379
494
|
|
|
495
|
+
📖 [Full docs](docs/methods/insertMany.md)
|
|
496
|
+
|
|
380
497
|
Inserts multiple records into the database in a single operation.
|
|
381
498
|
|
|
382
499
|
```typescript
|
|
@@ -414,6 +531,47 @@ const users = await insertMany({
|
|
|
414
531
|
})
|
|
415
532
|
```
|
|
416
533
|
|
|
534
|
+
### upsert
|
|
535
|
+
|
|
536
|
+
📖 [Full docs](docs/methods/upsert.md)
|
|
537
|
+
|
|
538
|
+
Inserts a record, or updates it in place when it collides with an existing unique/primary key constraint. `conflictFields` must name columns already covered by a **real unique or primary key constraint** on `tableName` — `upsert` does not create or verify that constraint, it only builds SQL that assumes it exists.
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
const result = await upsert<P, R>({
|
|
542
|
+
tableName: string,
|
|
543
|
+
dbClient: IDatabaseClient,
|
|
544
|
+
data: P,
|
|
545
|
+
conflictFields: string[], // must match a real unique/PK constraint
|
|
546
|
+
updateFields?: string[], // default: every field in `data`
|
|
547
|
+
returning?: string[]
|
|
548
|
+
})
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
#### Examples
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// Insert a user, or update name/age if the email already exists
|
|
555
|
+
// (requires: CREATE UNIQUE INDEX idx_users_email ON users(email);)
|
|
556
|
+
const user = await upsert({
|
|
557
|
+
tableName: 'users',
|
|
558
|
+
dbClient,
|
|
559
|
+
data: { email: 'john@example.com', name: 'John Doe', age: 30 },
|
|
560
|
+
conflictFields: ['email'],
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
// Only refresh `name` on conflict, leave other fields untouched
|
|
564
|
+
const user = await upsert({
|
|
565
|
+
tableName: 'users',
|
|
566
|
+
dbClient,
|
|
567
|
+
data: { email: 'john@example.com', name: 'John Doe', role: 'admin' },
|
|
568
|
+
conflictFields: ['email'],
|
|
569
|
+
updateFields: ['name'],
|
|
570
|
+
})
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
> On mysql, `conflictFields` is **not** part of the generated SQL — `ON DUPLICATE KEY UPDATE` relies entirely on the table's own constraint to detect the conflict. `conflictFields` there is used only to re-select the row afterwards, since mysql has no `RETURNING`.
|
|
574
|
+
|
|
417
575
|
### update
|
|
418
576
|
|
|
419
577
|
Updates a single record by ID.
|
|
@@ -486,12 +644,15 @@ const updatedUsers = await updateMany({
|
|
|
486
644
|
})
|
|
487
645
|
|
|
488
646
|
// Update with complex conditions
|
|
647
|
+
// updateMany() sets columns to the literal value passed in `data` — it does
|
|
648
|
+
// not interpret an { operator, value } shape as an arithmetic update. Read
|
|
649
|
+
// the current value first if you need to increment/decrement it.
|
|
489
650
|
const updatedUsers = await updateMany({
|
|
490
651
|
tableName: 'users',
|
|
491
652
|
dbClient,
|
|
492
653
|
data: {
|
|
493
654
|
last_login: new Date(),
|
|
494
|
-
login_count:
|
|
655
|
+
login_count: currentLoginCount + 1,
|
|
495
656
|
},
|
|
496
657
|
where: {
|
|
497
658
|
AND: [
|
|
@@ -571,6 +732,8 @@ await deleteMany({
|
|
|
571
732
|
|
|
572
733
|
### joins
|
|
573
734
|
|
|
735
|
+
📖 [Full docs](docs/methods/joins.md)
|
|
736
|
+
|
|
574
737
|
Executes queries with JOIN operations.
|
|
575
738
|
|
|
576
739
|
```typescript
|
|
@@ -637,15 +800,16 @@ const report = await joins({
|
|
|
637
800
|
},
|
|
638
801
|
],
|
|
639
802
|
groupBy: ['users.id', 'users.name', 'users.email', 'plans.name'],
|
|
640
|
-
having: {
|
|
641
|
-
'COUNT(orders.id)': { operator: '>', value: 0 },
|
|
642
|
-
},
|
|
643
803
|
orderBy: [{ field: 'total_spent', direction: 'DESC' }],
|
|
644
804
|
})
|
|
645
805
|
```
|
|
646
806
|
|
|
807
|
+
> `joins()` has no `having` parameter. Filter on the aggregate at the application layer, or use `rawQuery` if you need a real `HAVING` clause.
|
|
808
|
+
|
|
647
809
|
### rawQuery
|
|
648
810
|
|
|
811
|
+
📖 [Full docs](docs/methods/rawQuery.md)
|
|
812
|
+
|
|
649
813
|
Executes raw SQL queries directly on the database.
|
|
650
814
|
|
|
651
815
|
```typescript
|
|
@@ -689,6 +853,8 @@ const stats = await rawQuery({
|
|
|
689
853
|
|
|
690
854
|
## Transactions
|
|
691
855
|
|
|
856
|
+
📖 [Full docs](docs/methods/transactions.md)
|
|
857
|
+
|
|
692
858
|
Execute multiple database operations within a single transaction to ensure data consistency and atomicity.
|
|
693
859
|
|
|
694
860
|
### withTransaction
|
|
@@ -709,6 +875,7 @@ import {
|
|
|
709
875
|
withTransaction,
|
|
710
876
|
insert,
|
|
711
877
|
update,
|
|
878
|
+
findFirst,
|
|
712
879
|
} from '@starbemtech/star-db-query-builder'
|
|
713
880
|
|
|
714
881
|
// Create user with profile in a single transaction
|
|
@@ -764,13 +931,22 @@ const processOrder = async (orderData: any, orderItems: any[]) => {
|
|
|
764
931
|
|
|
765
932
|
totalAmount += item.price * item.quantity
|
|
766
933
|
|
|
767
|
-
//
|
|
934
|
+
// update() sets columns to the literal value passed in `data` — it
|
|
935
|
+
// does not interpret { operator, value } as an arithmetic update.
|
|
936
|
+
// Read the current stock first, then write the computed result.
|
|
937
|
+
const product = await findFirst({
|
|
938
|
+
tableName: 'products',
|
|
939
|
+
dbClient: tx,
|
|
940
|
+
select: ['stock'],
|
|
941
|
+
where: { id: { operator: '=', value: item.product_id } },
|
|
942
|
+
})
|
|
943
|
+
|
|
768
944
|
await update({
|
|
769
945
|
tableName: 'products',
|
|
770
946
|
dbClient: tx,
|
|
771
947
|
id: item.product_id,
|
|
772
948
|
data: {
|
|
773
|
-
stock:
|
|
949
|
+
stock: product.stock - item.quantity,
|
|
774
950
|
},
|
|
775
951
|
})
|
|
776
952
|
}
|
|
@@ -862,6 +1038,8 @@ interface ITransactionClient {
|
|
|
862
1038
|
|
|
863
1039
|
Used for building WHERE clauses with type safety.
|
|
864
1040
|
|
|
1041
|
+
> `IN`/`NOT IN`/`BETWEEN` accept at most **10,000** values in `value`. A larger array throws a descriptive error instead of building an oversized query — chunk the list (e.g. multiple `IN` queries, or `= ANY($1::type[])` on pg) instead of forwarding an unbounded list (e.g. raw search results) as a single condition. `BETWEEN` additionally requires exactly 2 values. Every condition must use the `{ operator, value }` shape — a plain value (e.g. `{ status: 'active' }`) throws instead of being silently dropped from the WHERE clause.
|
|
1042
|
+
|
|
865
1043
|
```typescript
|
|
866
1044
|
type Conditions<T> = {
|
|
867
1045
|
[P in keyof T]?: Condition<T[P]>
|
|
@@ -892,7 +1070,10 @@ interface OperatorCondition {
|
|
|
892
1070
|
interface LogicalCondition<T> {
|
|
893
1071
|
OR?: Conditions<T>[]
|
|
894
1072
|
AND?: Conditions<T>[]
|
|
895
|
-
|
|
1073
|
+
// Nested AND-group rendered as its own parenthesized clause, e.g.
|
|
1074
|
+
// `(a = $1 AND b = $2)`. Despite the name this has nothing to do with SQL
|
|
1075
|
+
// JOINs — see the `joins()` query function for that.
|
|
1076
|
+
JOINS?: Conditions<object>[]
|
|
896
1077
|
notExists?: OperatorCondition
|
|
897
1078
|
}
|
|
898
1079
|
```
|
|
@@ -953,19 +1134,6 @@ const users = await findMany({
|
|
|
953
1134
|
})
|
|
954
1135
|
```
|
|
955
1136
|
|
|
956
|
-
### Using Unaccent for PostgreSQL
|
|
957
|
-
|
|
958
|
-
```typescript
|
|
959
|
-
const users = await findMany({
|
|
960
|
-
tableName: 'users',
|
|
961
|
-
dbClient,
|
|
962
|
-
where: {
|
|
963
|
-
name: { operator: 'ILIKE', value: '%joão%' },
|
|
964
|
-
},
|
|
965
|
-
unaccent: true, // Enables unaccent search
|
|
966
|
-
})
|
|
967
|
-
```
|
|
968
|
-
|
|
969
1137
|
## Monitoring
|
|
970
1138
|
|
|
971
1139
|
The library provides a comprehensive monitoring system to track database operations and performance.
|
|
@@ -1358,78 +1526,11 @@ Always wrap database operations in try-catch blocks and handle errors appropriat
|
|
|
1358
1526
|
|
|
1359
1527
|
## Contributing
|
|
1360
1528
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
### Development Setup
|
|
1364
|
-
|
|
1365
|
-
1. **Clone the repository**
|
|
1366
|
-
|
|
1367
|
-
```bash
|
|
1368
|
-
git clone https://github.com/starbem/star-db-query-builder.git
|
|
1369
|
-
cd star-db-query-builder
|
|
1370
|
-
```
|
|
1371
|
-
|
|
1372
|
-
2. **Install dependencies**
|
|
1373
|
-
|
|
1374
|
-
```bash
|
|
1375
|
-
pnpm install
|
|
1376
|
-
```
|
|
1377
|
-
|
|
1378
|
-
3. **Run tests**
|
|
1379
|
-
|
|
1380
|
-
```bash
|
|
1381
|
-
pnpm test
|
|
1382
|
-
```
|
|
1383
|
-
|
|
1384
|
-
4. **Run linting**
|
|
1385
|
-
|
|
1386
|
-
```bash
|
|
1387
|
-
pnpm lint
|
|
1388
|
-
```
|
|
1389
|
-
|
|
1390
|
-
5. **Build the project**
|
|
1391
|
-
```bash
|
|
1392
|
-
pnpm build
|
|
1393
|
-
```
|
|
1394
|
-
|
|
1395
|
-
### Contributing Guidelines
|
|
1396
|
-
|
|
1397
|
-
- **Code Style**: Follow the existing code style and use Prettier for formatting
|
|
1398
|
-
- **TypeScript**: Maintain strict TypeScript typing
|
|
1399
|
-
- **Tests**: Add tests for new features and bug fixes
|
|
1400
|
-
- **Documentation**: Update documentation for any API changes
|
|
1401
|
-
- **Commit Messages**: Use conventional commit messages
|
|
1402
|
-
|
|
1403
|
-
### Pull Request Process
|
|
1404
|
-
|
|
1405
|
-
1. Fork the repository
|
|
1406
|
-
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
1407
|
-
3. Make your changes
|
|
1408
|
-
4. Add tests for your changes
|
|
1409
|
-
5. Ensure all tests pass (`pnpm test`)
|
|
1410
|
-
6. Run linting (`pnpm lint`)
|
|
1411
|
-
7. Commit your changes (`git commit -m 'feat: add amazing feature'`)
|
|
1412
|
-
8. Push to your branch (`git push origin feature/amazing-feature`)
|
|
1413
|
-
9. Open a Pull Request
|
|
1414
|
-
|
|
1415
|
-
### Reporting Issues
|
|
1416
|
-
|
|
1417
|
-
When reporting issues, please include:
|
|
1418
|
-
|
|
1419
|
-
- **Environment**: Node.js version, database type and version
|
|
1420
|
-
- **Steps to Reproduce**: Clear steps to reproduce the issue
|
|
1421
|
-
- **Expected Behavior**: What you expected to happen
|
|
1422
|
-
- **Actual Behavior**: What actually happened
|
|
1423
|
-
- **Code Sample**: Minimal code sample that demonstrates the issue
|
|
1424
|
-
|
|
1425
|
-
### Feature Requests
|
|
1426
|
-
|
|
1427
|
-
For feature requests, please:
|
|
1529
|
+
See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the development setup, branch/commit conventions, the required local gate before opening a PR, and the documentation-update rule. Full agent/contributor reference: **[AGENTS.md](./AGENTS.md)**.
|
|
1428
1530
|
|
|
1429
|
-
-
|
|
1430
|
-
- **
|
|
1431
|
-
-
|
|
1432
|
-
- **Alternatives**: Any alternative solutions you've considered
|
|
1531
|
+
- Found a bug or want a new feature? Use the [issue templates](.github/ISSUE_TEMPLATE/).
|
|
1532
|
+
- Found a security vulnerability? See **[SECURITY.md](./SECURITY.md)** — do not open a public issue.
|
|
1533
|
+
- This project follows the **[Code of Conduct](./CODE_OF_CONDUCT.md)**.
|
|
1433
1534
|
|
|
1434
1535
|
## License
|
|
1435
1536
|
|
|
@@ -1437,9 +1538,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
1437
1538
|
|
|
1438
1539
|
## Support
|
|
1439
1540
|
|
|
1440
|
-
- **Documentation**: [
|
|
1541
|
+
- **Documentation**: [docs/INDEX.md](docs/INDEX.md)
|
|
1441
1542
|
- **Issues**: [GitHub Issues](https://github.com/starbem/star-db-query-builder/issues)
|
|
1442
|
-
- **Discussions**: [GitHub Discussions](https://github.com/starbem/star-db-query-builder/discussions)
|
|
1443
1543
|
|
|
1444
1544
|
## Changelog
|
|
1445
1545
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Copies this package's bundled Claude Code skill into the consuming repo's
|
|
4
|
+
// .claude/skills/ so agents get correct star-db-query-builder usage guidance
|
|
5
|
+
// without a manual copy-paste. Run from the consuming repo's root:
|
|
6
|
+
//
|
|
7
|
+
// npx star-db-query-builder-install-skill
|
|
8
|
+
//
|
|
9
|
+
// Safe to re-run after upgrading the package — it always overwrites with the
|
|
10
|
+
// version bundled in the currently installed package.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
const path = require('path')
|
|
14
|
+
|
|
15
|
+
const SKILL_NAME = 'star-db-query-builder'
|
|
16
|
+
const SOURCE = path.join(__dirname, '..', '.claude', 'skills', SKILL_NAME, 'SKILL.md')
|
|
17
|
+
const TARGET_DIR = path.join(process.cwd(), '.claude', 'skills', SKILL_NAME)
|
|
18
|
+
const TARGET = path.join(TARGET_DIR, 'SKILL.md')
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
if (!fs.existsSync(SOURCE)) {
|
|
22
|
+
console.error(
|
|
23
|
+
`[star-db-query-builder] Could not find bundled skill at ${SOURCE}. ` +
|
|
24
|
+
'Is @starbemtech/star-db-query-builder installed correctly?'
|
|
25
|
+
)
|
|
26
|
+
process.exitCode = 1
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const alreadyExists = fs.existsSync(TARGET)
|
|
31
|
+
const previous = alreadyExists ? fs.readFileSync(TARGET, 'utf8') : null
|
|
32
|
+
const next = fs.readFileSync(SOURCE, 'utf8')
|
|
33
|
+
|
|
34
|
+
if (previous === next) {
|
|
35
|
+
console.log(`[star-db-query-builder] Skill already up to date at ${path.relative(process.cwd(), TARGET)}`)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(TARGET_DIR, { recursive: true })
|
|
40
|
+
fs.writeFileSync(TARGET, next)
|
|
41
|
+
|
|
42
|
+
console.log(
|
|
43
|
+
`[star-db-query-builder] ${alreadyExists ? 'Updated' : 'Installed'} skill at ${path.relative(
|
|
44
|
+
process.cwd(),
|
|
45
|
+
TARGET
|
|
46
|
+
)}`
|
|
47
|
+
)
|
|
48
|
+
if (!alreadyExists) {
|
|
49
|
+
console.log('[star-db-query-builder] Commit this file so the rest of the team gets it too.')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { QueryParams, RawQueryParams } from './types';
|
|
2
|
-
import { ITransactionClient } from '../db/IDatabaseClient';
|
|
1
|
+
import { QueryParams, RawQueryParams, CursorPageResult } from './types';
|
|
2
|
+
import { IDatabaseClient, ITransactionClient } from '../db/IDatabaseClient';
|
|
3
3
|
/**
|
|
4
4
|
* Finds the first record in the specified table
|
|
5
5
|
*
|
|
@@ -18,7 +18,7 @@ import { ITransactionClient } from '../db/IDatabaseClient';
|
|
|
18
18
|
* tableName: 'users',
|
|
19
19
|
* dbClient: dbClient,
|
|
20
20
|
* select: ['id', 'name', 'email'],
|
|
21
|
-
* where: { status: 'active' },
|
|
21
|
+
* where: { status: { operator: '=', value: 'active' } },
|
|
22
22
|
* groupBy: ['status'],
|
|
23
23
|
* orderBy: [{ field: 'created_at', direction: 'DESC' }],
|
|
24
24
|
* })
|
|
@@ -28,7 +28,7 @@ import { ITransactionClient } from '../db/IDatabaseClient';
|
|
|
28
28
|
* tableName: 'users',
|
|
29
29
|
* dbClient: dbClient,
|
|
30
30
|
* select: ['id', 'name', 'email'],
|
|
31
|
-
* where: { status: 'active' },
|
|
31
|
+
* where: { status: { operator: '=', value: 'active' } },
|
|
32
32
|
* groupBy: ['status'],
|
|
33
33
|
* orderBy: [{ field: 'created_at', direction: 'DESC' }],
|
|
34
34
|
* })
|
|
@@ -52,7 +52,7 @@ export declare const findFirst: <T>({ tableName, dbClient, select, where, groupB
|
|
|
52
52
|
* tableName: 'users',
|
|
53
53
|
* dbClient: dbClient,
|
|
54
54
|
* select: ['id', 'name', 'email'],
|
|
55
|
-
* where: { status: 'active' },
|
|
55
|
+
* where: { status: { operator: '=', value: 'active' } },
|
|
56
56
|
* groupBy: ['status'],
|
|
57
57
|
* orderBy: [{ field: 'created_at', direction: 'DESC' }],
|
|
58
58
|
* limit: 10,
|
|
@@ -61,6 +61,51 @@ export declare const findFirst: <T>({ tableName, dbClient, select, where, groupB
|
|
|
61
61
|
* })
|
|
62
62
|
*/
|
|
63
63
|
export declare const findMany: <T>({ tableName, dbClient, select, where, groupBy, orderBy, limit, offset, unaccent, }: QueryParams<T>) => Promise<T[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Finds multiple records in the specified table using keyset (cursor)
|
|
66
|
+
* pagination instead of offset/limit
|
|
67
|
+
*
|
|
68
|
+
* Unlike offset-based pagination, keyset pagination stays O(1) regardless of
|
|
69
|
+
* how deep the page is (no `OFFSET N` row-skipping) and does not skip/repeat
|
|
70
|
+
* rows when the underlying data changes between pages. It requires
|
|
71
|
+
* `cursorField` to be a strictly ordered, indexed column — `id` (if
|
|
72
|
+
* sequential) or `created_at` are typical choices; a plain UUID `id` works
|
|
73
|
+
* for uniqueness but its ordering is arbitrary, so prefer a monotonically
|
|
74
|
+
* increasing column when the page order matters to callers.
|
|
75
|
+
*
|
|
76
|
+
* @template T - The type of the records to be returned
|
|
77
|
+
* @param params - Query parameters including table name, database client, select fields, where conditions, cursor field, cursor value, direction, page size and unaccent
|
|
78
|
+
* @returns Promise<CursorPageResult<T>> - The page of records plus the cursor to request the next page (`null` when there is no next page)
|
|
79
|
+
*
|
|
80
|
+
* @throws {Error} When table name is not provided
|
|
81
|
+
* @throws {Error} When database client is not provided
|
|
82
|
+
* @throws {Error} When direction is not ASC or DESC
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // First page
|
|
86
|
+
* const page1 = await findManyCursor({
|
|
87
|
+
* tableName: 'users',
|
|
88
|
+
* dbClient: dbClient,
|
|
89
|
+
* where: { status: { operator: '=', value: 'active' } },
|
|
90
|
+
* cursorField: 'created_at',
|
|
91
|
+
* limit: 20,
|
|
92
|
+
* })
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Next page
|
|
96
|
+
* const page2 = await findManyCursor({
|
|
97
|
+
* tableName: 'users',
|
|
98
|
+
* dbClient: dbClient,
|
|
99
|
+
* cursorField: 'created_at',
|
|
100
|
+
* cursor: page1.nextCursor,
|
|
101
|
+
* limit: 20,
|
|
102
|
+
* })
|
|
103
|
+
*/
|
|
104
|
+
export declare const findManyCursor: <T>({ tableName, dbClient, select, where, cursorField, cursor, direction, limit, unaccent, }: QueryParams<T> & {
|
|
105
|
+
cursorField?: string;
|
|
106
|
+
cursor?: string | number;
|
|
107
|
+
direction?: "ASC" | "DESC";
|
|
108
|
+
}) => Promise<CursorPageResult<T>>;
|
|
64
109
|
/**
|
|
65
110
|
* Inserts a new record into the specified table
|
|
66
111
|
*
|
|
@@ -115,6 +160,44 @@ export declare const insertMany: <P, R>({ tableName, dbClient, data, returning,
|
|
|
115
160
|
data: P[];
|
|
116
161
|
returning?: string[];
|
|
117
162
|
}) => Promise<R[]>;
|
|
163
|
+
/**
|
|
164
|
+
* Inserts a record, or updates it in place if it collides with an existing
|
|
165
|
+
* unique/primary key
|
|
166
|
+
*
|
|
167
|
+
* Builds `INSERT ... ON CONFLICT (...) DO UPDATE SET ...` for pg and
|
|
168
|
+
* `INSERT ... ON DUPLICATE KEY UPDATE ...` for mysql. `conflictFields` must
|
|
169
|
+
* name columns actually covered by a unique or primary key constraint on
|
|
170
|
+
* `tableName` — this function does not create or verify that constraint, it
|
|
171
|
+
* only builds SQL that assumes it exists. For mysql, `conflictFields` is not
|
|
172
|
+
* part of the generated SQL (`ON DUPLICATE KEY UPDATE` relies on the table's
|
|
173
|
+
* own constraint) — it is used only to re-select the row afterwards, since
|
|
174
|
+
* mysql has no `RETURNING`.
|
|
175
|
+
*
|
|
176
|
+
* @template P - The type of the data to be upserted
|
|
177
|
+
* @template R - The type of the record to be returned
|
|
178
|
+
* @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
|
|
179
|
+
* @returns Promise<R> - The inserted or updated record
|
|
180
|
+
*
|
|
181
|
+
* @throws {Error} When table name is not provided
|
|
182
|
+
* @throws {Error} When database client is not provided
|
|
183
|
+
* @throws {Error} When data object is not provided
|
|
184
|
+
* @throws {Error} When conflictFields is not provided or empty
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const record = await upsert({
|
|
188
|
+
* tableName: 'users',
|
|
189
|
+
* dbClient: dbClient,
|
|
190
|
+
* data: { email: 'john.doe@example.com', name: 'John Doe' },
|
|
191
|
+
* conflictFields: ['email'],
|
|
192
|
+
* returning: ['id', 'name', 'email'],
|
|
193
|
+
* })
|
|
194
|
+
*/
|
|
195
|
+
export declare const upsert: <P, R>({ tableName, dbClient, data, conflictFields, updateFields, returning, }: QueryParams<R> & {
|
|
196
|
+
data: P;
|
|
197
|
+
conflictFields: string[];
|
|
198
|
+
updateFields?: string[];
|
|
199
|
+
returning?: string[];
|
|
200
|
+
}) => Promise<R>;
|
|
118
201
|
/**
|
|
119
202
|
* Updates a record in the specified table
|
|
120
203
|
*
|
|
@@ -164,7 +247,7 @@ export declare const update: <P, R>({ tableName, dbClient, id, data, returning,
|
|
|
164
247
|
* tableName: 'users',
|
|
165
248
|
* dbClient: dbClient,
|
|
166
249
|
* data: { name: 'John Doe', email: 'john.doe@example.com' },
|
|
167
|
-
* where: { status: 'active' },
|
|
250
|
+
* where: { status: { operator: '=', value: 'active' } },
|
|
168
251
|
* returning: ['id', 'name', 'email'],
|
|
169
252
|
* })
|
|
170
253
|
*/
|
|
@@ -247,7 +330,7 @@ export declare const deleteMany: <T>({ tableName, dbClient, ids, field, permanen
|
|
|
247
330
|
* dbClient: dbClient,
|
|
248
331
|
* select: ['id', 'name', 'email'],
|
|
249
332
|
* joins: [{ type: 'INNER', table: 'orders', on: 'users.id = orders.user_id' }],
|
|
250
|
-
* where: { status: 'active' },
|
|
333
|
+
* where: { status: { operator: '=', value: 'active' } },
|
|
251
334
|
* groupBy: ['status'],
|
|
252
335
|
* orderBy: [{ field: 'created_at', direction: 'DESC' }],
|
|
253
336
|
* limit: 10,
|
|
@@ -341,7 +424,7 @@ export declare const rawQuery: <T = any>({ dbClient, sql, params, }: RawQueryPar
|
|
|
341
424
|
* console.error('Transaction failed:', error)
|
|
342
425
|
* }
|
|
343
426
|
*/
|
|
344
|
-
export declare const withTransaction: <T>(dbClient:
|
|
427
|
+
export declare const withTransaction: <T>(dbClient: IDatabaseClient, transactionFn: (tx: ITransactionClient) => Promise<T>) => Promise<T>;
|
|
345
428
|
/**
|
|
346
429
|
* Creates a transaction client for manual transaction management
|
|
347
430
|
* @param dbClient - Database client instance
|
|
@@ -371,4 +454,4 @@ export declare const withTransaction: <T>(dbClient: any, transactionFn: (tx: ITr
|
|
|
371
454
|
* throw error
|
|
372
455
|
* }
|
|
373
456
|
*/
|
|
374
|
-
export declare const beginTransaction: (dbClient:
|
|
457
|
+
export declare const beginTransaction: (dbClient: IDatabaseClient) => Promise<ITransactionClient>;
|