@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.
Files changed (75) hide show
  1. package/.claude/skills/star-db-query-builder/SKILL.md +104 -0
  2. package/CHANGELOG.md +82 -45
  3. package/LICENSE +21 -0
  4. package/README.md +194 -94
  5. package/bin/install-skill.js +53 -0
  6. package/dist/src/core/repository.d.ts +92 -9
  7. package/dist/src/core/repository.js +275 -27
  8. package/dist/src/core/repository.js.map +1 -1
  9. package/dist/src/core/types.d.ts +16 -1
  10. package/dist/src/core/utils.d.ts +102 -0
  11. package/dist/src/core/utils.js +287 -70
  12. package/dist/src/core/utils.js.map +1 -1
  13. package/dist/src/db/initDb.d.ts +60 -50
  14. package/dist/src/db/initDb.js +96 -64
  15. package/dist/src/db/initDb.js.map +1 -1
  16. package/dist/src/db/mysqlClient.d.ts +3 -8
  17. package/dist/src/db/mysqlClient.js +9 -11
  18. package/dist/src/db/mysqlClient.js.map +1 -1
  19. package/dist/src/db/pgClient.js +0 -2
  20. package/dist/src/db/pgClient.js.map +1 -1
  21. package/dist/src/monitor/monitor.js +7 -0
  22. package/dist/src/monitor/monitor.js.map +1 -1
  23. package/package.json +27 -19
  24. package/.github/workflows/publish.yml +0 -118
  25. package/.prettierignore +0 -3
  26. package/.prettierrc +0 -5
  27. package/ARCHITECTURE.md +0 -313
  28. package/coverage/base.css +0 -224
  29. package/coverage/block-navigation.js +0 -87
  30. package/coverage/favicon.png +0 -0
  31. package/coverage/index.html +0 -131
  32. package/coverage/lcov-report/base.css +0 -224
  33. package/coverage/lcov-report/block-navigation.js +0 -87
  34. package/coverage/lcov-report/favicon.png +0 -0
  35. package/coverage/lcov-report/index.html +0 -131
  36. package/coverage/lcov-report/mysqlClient.ts.html +0 -685
  37. package/coverage/lcov-report/pgClient.ts.html +0 -823
  38. package/coverage/lcov-report/prettify.css +0 -1
  39. package/coverage/lcov-report/prettify.js +0 -2
  40. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  41. package/coverage/lcov-report/sorter.js +0 -210
  42. package/coverage/lcov.info +0 -533
  43. package/coverage/mysqlClient.ts.html +0 -685
  44. package/coverage/pgClient.ts.html +0 -823
  45. package/coverage/prettify.css +0 -1
  46. package/coverage/prettify.js +0 -2
  47. package/coverage/sort-arrow-sprite.png +0 -0
  48. package/coverage/sorter.js +0 -210
  49. package/dist/src/setupTests.d.ts +0 -26
  50. package/dist/src/setupTests.js +0 -43
  51. package/dist/src/setupTests.js.map +0 -1
  52. package/docs/INDEX.md +0 -145
  53. package/docs/methods/findFirst.md +0 -394
  54. package/docs/methods/findMany.md +0 -587
  55. package/docs/methods/insert.md +0 -536
  56. package/docs/methods/insertMany.md +0 -627
  57. package/docs/methods/joins.md +0 -781
  58. package/docs/methods/rawQuery.md +0 -284
  59. package/docs/methods/transactions.md +0 -737
  60. package/eslint.config.mjs +0 -77
  61. package/index.ts +0 -16
  62. package/jest.config.ts +0 -194
  63. package/scripts/release.sh +0 -123
  64. package/src/core/repository.ts +0 -865
  65. package/src/core/types.ts +0 -97
  66. package/src/core/utils.ts +0 -357
  67. package/src/db/IDatabaseClient.ts +0 -16
  68. package/src/db/__tests__/mysqlClient.test.ts +0 -262
  69. package/src/db/__tests__/pgClient.test.ts +0 -260
  70. package/src/db/initDb.ts +0 -181
  71. package/src/db/mysqlClient.ts +0 -200
  72. package/src/db/pgClient.ts +0 -246
  73. package/src/monitor/monitor.ts +0 -16
  74. package/src/setupTests.ts +0 -45
  75. 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: { operator: '+', value: 1 },
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
- // Update product stock
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: { operator: '-', value: item.quantity },
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
- JOINS?: Conditions<object>
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
- We welcome contributions to the Star DB Query Builder! Here's how you can help:
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
- - **Describe the feature**: Clear description of what you want
1430
- - **Use Case**: Explain why this feature would be useful
1431
- - **Proposed API**: If you have ideas for the API design
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**: [GitHub Wiki](https://github.com/starbem/star-db-query-builder/wiki)
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: any, transactionFn: (tx: ITransactionClient) => Promise<T>) => Promise<T>;
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: any) => Promise<ITransactionClient>;
457
+ export declare const beginTransaction: (dbClient: IDatabaseClient) => Promise<ITransactionClient>;