@starbemtech/star-db-query-builder 1.4.0 โ†’ 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/README.md CHANGED
@@ -1,1550 +1,151 @@
1
- # Star DB Query Builder
1
+ # @starbemtech/star-db-query-builder
2
2
 
3
- A powerful and flexible database query builder library for Node.js applications, supporting PostgreSQL and MySQL databases with TypeScript support.
3
+ ## Overview
4
4
 
5
- ## Table of Contents
5
+ A TypeScript query-builder library shared across Starbem's backend microservices for building and executing PostgreSQL and MySQL queries alongside Prisma. It exposes a generic, type-safe repository API (`findFirst`, `findMany`, `insert`, `update`, `joins`, transactions, etc.) plus connection management for named PostgreSQL and MySQL pools. It is not a deployed service โ€” it's published to npm and imported as a dependency by services such as accounts-ms, doctors-ms, and partner-service.
6
6
 
7
- - [Features](#-features)
8
- - [Installation](#-installation)
9
- - [Quick Start](#-quick-start)
10
- - [Database Initialization](#database-initialization)
11
- - [Query Methods](#query-methods)
12
- - [findFirst](#findfirst)
13
- - [findMany](#findmany)
14
- - [findManyCursor](#findmanycursor)
15
- - [insert](#insert)
16
- - [insertMany](#insertmany)
17
- - [upsert](#upsert)
18
- - [update](#update)
19
- - [updateMany](#updatemany)
20
- - [deleteOne](#deleteone)
21
- - [deleteMany](#deletemany)
22
- - [joins](#joins)
23
- - [rawQuery](#rawquery)
24
- - [Transactions](#transactions)
25
- - [withTransaction](#withtransaction)
26
- - [beginTransaction](#begintransaction)
27
- - [Types and Interfaces](#types-and-interfaces)
28
- - [Advanced Usage](#advanced-usage)
29
- - [Monitoring](#monitoring)
30
- - [Best Practices](#best-practices)
31
- - [Error Handling](#error-handling)
32
- - [Contributing](#contributing)
33
- - [License](#license)
7
+ ## Tech Stack
34
8
 
35
- ## ๐Ÿ“š Full Documentation
9
+ - TypeScript, compiled with `tsc` (Node.js >= 18)
10
+ - `pg` (PostgreSQL driver) and `mysql2` (MySQL driver)
11
+ - `promise-retry` for automatic retry on transient query/connection errors
12
+ - `uuid` for generating record IDs on insert/upsert
13
+ - Jest + `ts-jest` for testing
14
+ - ESLint (flat config, `typescript-eslint`) + Prettier for linting/formatting
15
+ - Husky + `lint-staged` for pre-commit checks
16
+ - pnpm (pinned to `8.6.2`) as package manager
36
17
 
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:
18
+ ## Architecture
38
19
 
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)
20
+ The library is organized into three modules under `src/`:
42
21
 
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.
22
+ - **`src/core`** โ€” the generic repository (`repository.ts`) with all CRUD/query functions, shared TypeScript types (`types.ts`), and SQL-building utilities (`utils.ts`) that assemble `WHERE`/`SET`/`ORDER BY`/`GROUP BY`/`LIMIT`/`OFFSET` clauses and enforce identifier/SQL-fragment safety (`assertValidIdentifier`, `assertSafeSqlFragment`, `assertNoAutoManagedColumns`, `assertWithinBindParamLimit`).
23
+ - **`src/db`** โ€” connection management. `initDb.ts` creates and registers named PostgreSQL/MySQL pools (with optional retry options, query timeout, and the `unaccent` extension for pg), `pgClient.ts`/`mysqlClient.ts` are the concrete client implementations, and `IDatabaseClient.ts` defines the shared client/transaction contract.
24
+ - **`src/monitor`** โ€” an `EventEmitter`-based instance (`monitor`) emitting `CONNECTION_CREATED`, `QUERY_START`, `QUERY_END`, `QUERY_ERROR`, and `RETRY_ATTEMPT` events for observability.
44
25
 
45
- ## โœจ Features
26
+ `index.ts` is the sole public entry point, re-exporting `initDb`/`getDbClient`/`closeDb`/etc. from `src/db`, all repository functions from `src/core/repository`, the `monitor` instance, and the public TypeScript types.
46
27
 
47
- ### ๐Ÿ”ง **Core Functionality**
28
+ Note: `ARCHITECTURE.md` in this repo references an older `default/genericRepository.ts` layout โ€” the current source of truth is `src/core/`, `src/db/`, and `src/monitor/` as described above.
48
29
 
49
- - **๐Ÿ”„ Multi-Connection Support**: Connect simultaneously to multiple PostgreSQL and MySQL databases
50
- - **๐Ÿ›ก๏ธ Type Safety**: Complete TypeScript support with strong typing
51
- - **โšก Auto Retry**: Automatic retry for transient errors (timeouts, lost connections)
52
- - **๐Ÿ“Š Monitoring**: Event system for monitoring and logging
53
- - **๐Ÿ” Query Builder**: Fluent interface for building complex queries
54
- - **๐Ÿ“ฆ Batch Operations**: Optimized batch operations (insertMany, updateMany)
30
+ ## Security & Operational Notes
55
31
 
56
- ### ๐Ÿ—„๏ธ **Database Support**
32
+ - **`monitor` events carry raw query parameters.** `QUERY_START`/`QUERY_END`/`QUERY_ERROR` include the unredacted `params` array passed to `dbClient.query(...)` โ€” the same values bound into the SQL (user emails, document numbers, health data depending on what a service queries by). If your service attaches a listener that logs or forwards these events (e.g. to Datadog/Sentry), redact or drop `params` before persisting the event anywhere subject to LGPD, rather than logging the monitor payload as-is.
33
+ - **Automatic retry is not aware of query idempotency.** `retryOptions` (on `initDb`/`createPgClient`/`createMysqlClient`) retries any query โ€” including `rawQuery` and non-idempotent statements like `UPDATE ... SET n = n + 1` โ€” on a transient connection error (`ECONNRESET`, `ETIMEDOUT`, etc.). If the original attempt's statement actually reached the database before the connection dropped, a retry can apply it twice. Prefer idempotent statements (keyed upserts, conditional updates) for anything run under `retryOptions`, or disable retries for statements where a double-apply would be unsafe.
57
34
 
58
- - **PostgreSQL**: Complete support with extensions (unaccent)
59
- - **MySQL**: Full compatibility with MySQL 5.7+
60
- - **Connection Pooling**: Efficient connection management
61
- - **Transaction Support**: Full ACID transaction support with automatic rollback
62
- - **Raw SQL**: Execute custom SQL queries when needed
35
+ ## Folder Structure
63
36
 
64
- ### ๐Ÿ› ๏ธ **Development Tools**
65
-
66
- - **ESLint + Prettier**: Clean and consistent code
67
- - **Jest**: Unit and integration tests
68
- - **Husky**: Git hooks for code quality
69
- - **TypeScript**: Compilation and typing
70
-
71
- ## ๐Ÿ“ฆ Installation
72
-
73
- ```bash
74
- npm install @starbemtech/star-db-query-builder
75
- # or
76
- pnpm add @starbemtech/star-db-query-builder
77
- # or
78
- yarn add @starbemtech/star-db-query-builder
37
+ ```
38
+ src/
39
+ โ”œโ”€โ”€ core/ # generic repository, types, and SQL-building/validation utilities
40
+ โ”‚ โ””โ”€โ”€ __tests__/ # unit tests for repository.ts and utils.ts
41
+ โ”œโ”€โ”€ db/ # PostgreSQL/MySQL client implementations and pool initialization
42
+ โ”‚ โ””โ”€โ”€ __tests__/ # unit tests for initDb, pgClient, mysqlClient
43
+ โ”œโ”€โ”€ monitor/ # EventEmitter-based query/connection event system
44
+ โ”‚ โ””โ”€โ”€ __tests__/ # unit tests for monitor.ts
45
+ โ””โ”€โ”€ setupTests.ts # Jest test setup
79
46
  ```
80
47
 
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:
48
+ ## Installation
84
49
 
85
50
  ```bash
86
- npx star-db-query-builder-install-skill
51
+ pnpm add @starbemtech/star-db-query-builder
87
52
  ```
88
53
 
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
-
91
- ## Quick Start
54
+ ## Usage
92
55
 
93
56
  ```typescript
94
57
  import {
95
58
  initDb,
96
59
  getDbClient,
97
60
  findFirst,
61
+ findMany,
98
62
  insert,
63
+ update,
64
+ withTransaction,
99
65
  } from '@starbemtech/star-db-query-builder'
100
66
 
101
- // Initialize database connection
102
- await initDb({
103
- type: 'pg', // or 'mysql'
104
- options: {
105
- host: 'localhost',
106
- port: 5432,
107
- database: 'myapp',
108
- user: 'username',
109
- password: 'password',
110
- },
111
- })
112
-
113
- // Get database client
114
- const dbClient = getDbClient()
115
-
116
- // Find a user
117
- const user = await findFirst({
118
- tableName: 'users',
119
- dbClient,
120
- where: { email: { operator: '=', value: 'user@example.com' } },
121
- })
122
-
123
- // Insert a new user
124
- const newUser = await insert({
125
- tableName: 'users',
126
- dbClient,
127
- data: { name: 'John Doe', email: 'john@example.com' },
128
- })
129
- ```
130
-
131
- ## Database Initialization
132
-
133
- ### initDb
134
-
135
- Initializes a database connection with the specified configuration.
136
-
137
- ```typescript
138
- await initDb({
139
- name?: string, // Optional client name (default: 'default')
140
- type: 'pg' | 'mysql', // Database type
141
- options: PoolConfig | MySqlPoolOptions, // Connection options
142
- retryOptions?: RetryOptions, // Optional retry configuration
143
- installUnaccentExtension?: boolean, // PostgreSQL unaccent extension
144
- queryTimeout?: number // Optional query timeout in ms (see below)
145
- })
146
- ```
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
-
150
- #### PostgreSQL Example
151
-
152
- ```typescript
153
- import { initDb } from '@starbemtech/star-db-query-builder'
154
-
67
+ // Initialize a named PostgreSQL connection (call once at startup)
155
68
  await initDb({
156
- name: 'main',
69
+ name: 'default',
157
70
  type: 'pg',
158
71
  options: {
159
- host: 'localhost',
72
+ host: process.env.DB_HOST,
160
73
  port: 5432,
161
- database: 'myapp',
162
- user: 'postgres',
163
- password: 'password',
164
- max: 20,
165
- idleTimeoutMillis: 30000,
166
- connectionTimeoutMillis: 2000,
167
- },
168
- retryOptions: {
169
- retries: 3,
170
- factor: 2,
171
- minTimeout: 1000,
172
- maxTimeout: 5000,
173
- },
174
- installUnaccentExtension: true,
175
- })
176
- ```
177
-
178
- #### MySQL Example
179
-
180
- ```typescript
181
- import { initDb } from '@starbemtech/star-db-query-builder'
182
-
183
- await initDb({
184
- name: 'analytics',
185
- type: 'mysql',
186
- options: {
187
- host: 'localhost',
188
- port: 3306,
189
- database: 'analytics',
190
- user: 'root',
191
- password: 'password',
192
- connectionLimit: 10,
193
- acquireTimeout: 60000,
194
- timeout: 60000,
195
- },
196
- })
197
- ```
198
-
199
- ### getDbClient
200
-
201
- Retrieves a database client by name.
202
-
203
- ```typescript
204
- const dbClient = getDbClient(name?: string)
205
- ```
206
-
207
- ```typescript
208
- // Get default client
209
- const defaultClient = getDbClient()
210
-
211
- // Get named client
212
- const analyticsClient = getDbClient('analytics')
213
- ```
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
-
253
- ## Query Methods
254
-
255
- ### findFirst
256
-
257
- ๐Ÿ“– [Full docs](docs/methods/findFirst.md)
258
-
259
- Finds the first record that matches the specified conditions.
260
-
261
- ```typescript
262
- const result = await findFirst<T>({
263
- tableName: string,
264
- dbClient: IDatabaseClient,
265
- select?: string[],
266
- where?: Conditions<T>,
267
- groupBy?: string[],
268
- orderBy?: OrderBy
269
- })
270
- ```
271
-
272
- #### Examples
273
-
274
- ```typescript
275
- // Find user by email
276
- const user = await findFirst({
277
- tableName: 'users',
278
- dbClient,
279
- where: {
280
- email: { operator: '=', value: 'user@example.com' },
281
- },
282
- })
283
-
284
- // Find with specific fields
285
- const user = await findFirst({
286
- tableName: 'users',
287
- dbClient,
288
- select: ['id', 'name', 'email'],
289
- where: {
290
- status: { operator: '=', value: 'active' },
291
- },
292
- })
293
-
294
- // Find with complex conditions
295
- const user = await findFirst({
296
- tableName: 'users',
297
- dbClient,
298
- where: {
299
- AND: [
300
- { email: { operator: '=', value: 'user@example.com' } },
301
- { status: { operator: '=', value: 'active' } },
302
- ],
303
- },
304
- })
305
-
306
- // Find with ordering
307
- const latestUser = await findFirst({
308
- tableName: 'users',
309
- dbClient,
310
- orderBy: [{ field: 'created_at', direction: 'DESC' }],
311
- })
312
- ```
313
-
314
- ### findMany
315
-
316
- ๐Ÿ“– [Full docs](docs/methods/findMany.md)
317
-
318
- Finds multiple records that match the specified conditions.
319
-
320
- ```typescript
321
- const results = await findMany<T>({
322
- tableName: string,
323
- dbClient: IDatabaseClient,
324
- select?: string[],
325
- where?: Conditions<T>,
326
- groupBy?: string[],
327
- orderBy?: OrderBy,
328
- limit?: number,
329
- offset?: number,
330
- unaccent?: boolean
331
- })
332
- ```
333
-
334
- #### Examples
335
-
336
- ```typescript
337
- // Find all active users
338
- const users = await findMany({
339
- tableName: 'users',
340
- dbClient,
341
- where: {
342
- status: { operator: '=', value: 'active' },
343
- },
344
- })
345
-
346
- // Find with pagination
347
- const users = await findMany({
348
- tableName: 'users',
349
- dbClient,
350
- limit: 10,
351
- offset: 20,
352
- orderBy: [{ field: 'created_at', direction: 'DESC' }],
353
- })
354
-
355
- // Find with complex conditions
356
- const users = await findMany({
357
- tableName: 'users',
358
- dbClient,
359
- where: {
360
- OR: [
361
- { status: { operator: '=', value: 'active' } },
362
- { status: { operator: '=', value: 'pending' } },
363
- ],
364
- created_at: {
365
- operator: '>=',
366
- value: new Date('2023-01-01'),
367
- },
74
+ database: process.env.DB_NAME,
75
+ user: process.env.DB_USER,
76
+ password: process.env.DB_PASSWORD,
368
77
  },
78
+ retryOptions: { retries: 3 },
369
79
  })
370
80
 
371
- // Find with grouping
372
- const userStats = await findMany({
373
- tableName: 'users',
374
- dbClient,
375
- select: ['status', 'COUNT(*) as count'],
376
- groupBy: ['status'],
377
- })
378
- ```
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
81
+ const dbClient = getDbClient('default')
401
82
 
402
- ```typescript
403
- // First page
404
- const page1 = await findManyCursor({
83
+ // Query
84
+ const user = await findFirst<{ id: string; name: string }>({
405
85
  tableName: 'users',
406
86
  dbClient,
87
+ select: ['id', 'name'],
407
88
  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
-
426
- ### insert
427
-
428
- ๐Ÿ“– [Full docs](docs/methods/insert.md)
429
-
430
- Inserts a single record into the database.
431
-
432
- ```typescript
433
- const result = await insert<P, R>({
434
- tableName: string,
435
- dbClient: IDatabaseClient,
436
- data: P,
437
- returning?: string[]
438
- })
439
- ```
440
-
441
- #### Examples
442
-
443
- ```typescript
444
- // Simple insert
445
- const user = await insert({
446
- tableName: 'users',
447
- dbClient,
448
- data: {
449
- name: 'John Doe',
450
- email: 'john@example.com',
451
- age: 30,
452
- },
453
- })
454
-
455
- // Insert with specific returning fields
456
- const user = await insert({
457
- tableName: 'users',
458
- dbClient,
459
- data: {
460
- name: 'Jane Doe',
461
- email: 'jane@example.com',
462
- },
463
- returning: ['id', 'name', 'email', 'created_at'],
464
- })
465
-
466
- // Insert with TypeScript typing
467
- interface UserData {
468
- name: string
469
- email: string
470
- age: number
471
- }
472
-
473
- interface User {
474
- id: string
475
- name: string
476
- email: string
477
- age: number
478
- created_at: Date
479
- updated_at: Date
480
- }
481
-
482
- const user: User = await insert<UserData, User>({
483
- tableName: 'users',
484
- dbClient,
485
- data: {
486
- name: 'John Doe',
487
- email: 'john@example.com',
488
- age: 30,
489
- },
490
- })
491
- ```
492
-
493
- ### insertMany
494
-
495
- ๐Ÿ“– [Full docs](docs/methods/insertMany.md)
496
-
497
- Inserts multiple records into the database in a single operation.
498
-
499
- ```typescript
500
- const results = await insertMany<P, R>({
501
- tableName: string,
502
- dbClient: IDatabaseClient,
503
- data: P[],
504
- returning?: string[]
505
- })
506
- ```
507
-
508
- #### Examples
509
-
510
- ```typescript
511
- // Insert multiple users
512
- const users = await insertMany({
513
- tableName: 'users',
514
- dbClient,
515
- data: [
516
- { name: 'John Doe', email: 'john@example.com' },
517
- { name: 'Jane Doe', email: 'jane@example.com' },
518
- { name: 'Bob Smith', email: 'bob@example.com' },
519
- ],
520
89
  })
521
90
 
522
- // Insert with returning fields
523
- const users = await insertMany({
91
+ // Insert
92
+ const created = await insert({
524
93
  tableName: 'users',
525
94
  dbClient,
526
- data: [
527
- { name: 'John Doe', email: 'john@example.com' },
528
- { name: 'Jane Doe', email: 'jane@example.com' },
529
- ],
95
+ data: { name: 'John Doe', email: 'john.doe@example.com' },
530
96
  returning: ['id', 'name', 'email'],
531
97
  })
532
- ```
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
-
575
- ### update
576
-
577
- Updates a single record by ID.
578
-
579
- ```typescript
580
- const result = await update<P, R>({
581
- tableName: string,
582
- dbClient: IDatabaseClient,
583
- id: string,
584
- data: P,
585
- returning?: string[]
586
- })
587
- ```
588
-
589
- #### Examples
590
-
591
- ```typescript
592
- // Simple update
593
- const updatedUser = await update({
594
- tableName: 'users',
595
- dbClient,
596
- id: 'user-123',
597
- data: {
598
- name: 'John Updated',
599
- age: 31,
600
- },
601
- })
602
-
603
- // Update with returning fields
604
- const updatedUser = await update({
605
- tableName: 'users',
606
- dbClient,
607
- id: 'user-123',
608
- data: {
609
- status: 'active',
610
- last_login: new Date(),
611
- },
612
- returning: ['id', 'status', 'last_login', 'updated_at'],
613
- })
614
- ```
615
98
 
616
- ### updateMany
617
-
618
- Updates multiple records based on specified conditions.
619
-
620
- ```typescript
621
- const results = await updateMany<P, R>({
622
- tableName: string,
623
- dbClient: IDatabaseClient,
624
- data: P,
625
- where: Conditions<T>,
626
- returning?: string[]
99
+ // Transaction
100
+ await withTransaction(dbClient, async (tx) => {
101
+ const inserted = await insert({ tableName: 'orders', dbClient: tx, data: { total: 100 } })
102
+ await update({ tableName: 'users', dbClient: tx, id: 'user-id', data: { last_order_id: inserted.id } })
627
103
  })
628
104
  ```
629
105
 
630
- #### Examples
631
-
632
- ```typescript
633
- // Update all inactive users
634
- const updatedUsers = await updateMany({
635
- tableName: 'users',
636
- dbClient,
637
- data: {
638
- status: 'active',
639
- updated_at: new Date(),
640
- },
641
- where: {
642
- status: { operator: '=', value: 'inactive' },
643
- },
644
- })
106
+ The full set of exported functions (`findFirst`, `findMany`, `findManyCursor`, `insert`, `insertMany`, `upsert`, `update`, `updateMany`, `deleteOne`, `deleteMany`, `joins`, `rawQuery`, `withTransaction`, `beginTransaction`) is documented with JSDoc in `src/core/repository.ts` and in `docs/methods/`.
645
107
 
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.
650
- const updatedUsers = await updateMany({
651
- tableName: 'users',
652
- dbClient,
653
- data: {
654
- last_login: new Date(),
655
- login_count: currentLoginCount + 1,
656
- },
657
- where: {
658
- AND: [
659
- { status: { operator: '=', value: 'active' } },
660
- { last_login: { operator: '<', value: new Date('2023-01-01') } },
661
- ],
662
- },
663
- returning: ['id', 'name', 'last_login', 'login_count'],
664
- })
665
- ```
108
+ ## Available Scripts
666
109
 
667
- ### deleteOne
110
+ | Script | Description |
111
+ |---|---|
112
+ | `build` | Compiles TypeScript to `dist/` via `tsc` (runs `clean` first) |
113
+ | `test` | Runs the Jest test suite |
114
+ | `test:watch` | Runs Jest in watch mode |
115
+ | `test:coverage` | Runs Jest with coverage report |
116
+ | `test:ci` | Runs Jest in CI mode (`--ci --coverage --watchAll=false`) |
117
+ | `lint` | Lints `src/**/*.ts` with ESLint |
118
+ | `lint:fix` | Lints and auto-fixes `src/**/*.ts` |
119
+ | `format` | Formats `src/**/*.ts` with Prettier |
120
+ | `format:check` | Checks formatting without writing changes |
121
+ | `type:check` | Type-checks without emitting (`tsc --noEmit`) |
122
+ | `clean` | Removes `dist` and `coverage` |
123
+ | `version:patch` / `version:minor` / `version:major` | Bumps version via `npm version` |
124
+ | `release` | Runs the interactive release helper (`scripts/release.sh`) |
125
+ | `release:patch` / `release:minor` / `release:major` | Bumps version and pushes tags |
126
+ | `prepublishOnly` | Clean, build, and `test:ci`, run automatically before `npm publish` |
668
127
 
669
- Deletes a single record by ID (soft delete by default).
128
+ ## Testing
670
129
 
671
- ```typescript
672
- await deleteOne<T>({
673
- tableName: string,
674
- dbClient: IDatabaseClient,
675
- id: string,
676
- permanently?: boolean
677
- })
130
+ ```bash
131
+ pnpm test # run the suite
132
+ pnpm test:watch # watch mode
133
+ pnpm test:coverage
134
+ pnpm test:ci # CI mode, used by ci.yml and release.yml
678
135
  ```
679
136
 
680
- #### Examples
681
-
682
- ```typescript
683
- // Soft delete (sets status to 'deleted')
684
- await deleteOne({
685
- tableName: 'users',
686
- dbClient,
687
- id: 'user-123',
688
- })
689
-
690
- // Permanent delete
691
- await deleteOne({
692
- tableName: 'users',
693
- dbClient,
694
- id: 'user-123',
695
- permanently: true,
696
- })
697
- ```
698
-
699
- ### deleteMany
700
-
701
- Deletes multiple records by IDs (soft delete by default).
702
-
703
- ```typescript
704
- await deleteMany<T>({
705
- tableName: string,
706
- dbClient: IDatabaseClient,
707
- ids: string[] | number[],
708
- field?: string,
709
- permanently?: boolean
710
- })
711
- ```
712
-
713
- #### Examples
714
-
715
- ```typescript
716
- // Soft delete multiple users
717
- await deleteMany({
718
- tableName: 'users',
719
- dbClient,
720
- ids: ['user-1', 'user-2', 'user-3'],
721
- })
722
-
723
- // Permanent delete with custom field
724
- await deleteMany({
725
- tableName: 'orders',
726
- dbClient,
727
- ids: [1, 2, 3],
728
- field: 'order_id',
729
- permanently: true,
730
- })
731
- ```
732
-
733
- ### joins
734
-
735
- ๐Ÿ“– [Full docs](docs/methods/joins.md)
736
-
737
- Executes queries with JOIN operations.
738
-
739
- ```typescript
740
- const results = await joins<T>({
741
- tableName: string,
742
- dbClient: IDatabaseClient,
743
- select: string[],
744
- joins: JoinClause[],
745
- where?: Conditions<T>,
746
- groupBy?: string[],
747
- orderBy?: OrderBy,
748
- limit?: number,
749
- offset?: number,
750
- unaccent?: boolean
751
- })
752
- ```
753
-
754
- #### Examples
755
-
756
- ```typescript
757
- // Simple JOIN
758
- const usersWithOrders = await joins({
759
- tableName: 'users',
760
- dbClient,
761
- select: ['users.id', 'users.name', 'orders.total'],
762
- joins: [
763
- {
764
- type: 'LEFT',
765
- table: 'orders',
766
- on: 'users.id = orders.user_id',
767
- },
768
- ],
769
- where: {
770
- 'users.status': { operator: '=', value: 'active' },
771
- },
772
- })
773
-
774
- // Multiple JOINs
775
- const report = await joins({
776
- tableName: 'users',
777
- dbClient,
778
- select: [
779
- 'users.name',
780
- 'users.email',
781
- 'COUNT(orders.id) as order_count',
782
- 'SUM(orders.total) as total_spent',
783
- 'plans.name as plan_name',
784
- ],
785
- joins: [
786
- {
787
- type: 'LEFT',
788
- table: 'orders',
789
- on: 'users.id = orders.user_id',
790
- },
791
- {
792
- type: 'LEFT',
793
- table: 'user_plans',
794
- on: 'users.id = user_plans.user_id',
795
- },
796
- {
797
- type: 'LEFT',
798
- table: 'plans',
799
- on: 'user_plans.plan_id = plans.id',
800
- },
801
- ],
802
- groupBy: ['users.id', 'users.name', 'users.email', 'plans.name'],
803
- orderBy: [{ field: 'total_spent', direction: 'DESC' }],
804
- })
805
- ```
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
-
809
- ### rawQuery
810
-
811
- ๐Ÿ“– [Full docs](docs/methods/rawQuery.md)
812
-
813
- Executes raw SQL queries directly on the database.
814
-
815
- ```typescript
816
- const result = await rawQuery<T>({
817
- dbClient: IDatabaseClient,
818
- sql: string,
819
- params?: any[]
820
- })
821
- ```
822
-
823
- #### Examples
824
-
825
- ```typescript
826
- // Simple raw query
827
- const users = await rawQuery({
828
- dbClient,
829
- sql: 'SELECT * FROM users WHERE active = true',
830
- })
831
-
832
- // Raw query with parameters
833
- const user = await rawQuery({
834
- dbClient,
835
- sql: 'SELECT * FROM users WHERE id = ? AND email = ?',
836
- params: ['user-123', 'user@example.com'],
837
- })
838
-
839
- // Complex aggregation
840
- const stats = await rawQuery({
841
- dbClient,
842
- sql: `
843
- SELECT
844
- COUNT(*) as total_users,
845
- AVG(age) as avg_age,
846
- MAX(created_at) as last_created
847
- FROM users
848
- WHERE created_at >= ?
849
- `,
850
- params: [new Date('2023-01-01')],
851
- })
852
- ```
853
-
854
- ## Transactions
855
-
856
- ๐Ÿ“– [Full docs](docs/methods/transactions.md)
857
-
858
- Execute multiple database operations within a single transaction to ensure data consistency and atomicity.
859
-
860
- ### withTransaction
861
-
862
- Executes a function within a database transaction with automatic commit/rollback handling.
863
-
864
- ```typescript
865
- const result = await withTransaction<T>(
866
- dbClient: IDatabaseClient,
867
- transactionFn: (tx: ITransactionClient) => Promise<T>
868
- ): Promise<T>
869
- ```
870
-
871
- #### Examples
872
-
873
- ```typescript
874
- import {
875
- withTransaction,
876
- insert,
877
- update,
878
- findFirst,
879
- } from '@starbemtech/star-db-query-builder'
880
-
881
- // Create user with profile in a single transaction
882
- const createUserWithProfile = async (userData: any, profileData: any) => {
883
- return withTransaction(dbClient, async (tx) => {
884
- // Create user
885
- const user = await insert({
886
- tableName: 'users',
887
- dbClient: tx,
888
- data: userData,
889
- })
890
-
891
- // Create user profile
892
- const profile = await insert({
893
- tableName: 'user_profiles',
894
- dbClient: tx,
895
- data: {
896
- ...profileData,
897
- user_id: user.id,
898
- },
899
- })
900
-
901
- return { user, profile }
902
- })
903
- }
904
-
905
- // E-commerce order processing
906
- const processOrder = async (orderData: any, orderItems: any[]) => {
907
- return withTransaction(dbClient, async (tx) => {
908
- // Create order
909
- const order = await insert({
910
- tableName: 'orders',
911
- dbClient: tx,
912
- data: {
913
- ...orderData,
914
- status: 'pending',
915
- total: 0,
916
- },
917
- })
918
-
919
- let totalAmount = 0
920
-
921
- // Create order items and calculate total
922
- for (const item of orderItems) {
923
- await insert({
924
- tableName: 'order_items',
925
- dbClient: tx,
926
- data: {
927
- ...item,
928
- order_id: order.id,
929
- },
930
- })
931
-
932
- totalAmount += item.price * item.quantity
933
-
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
-
944
- await update({
945
- tableName: 'products',
946
- dbClient: tx,
947
- id: item.product_id,
948
- data: {
949
- stock: product.stock - item.quantity,
950
- },
951
- })
952
- }
953
-
954
- // Update order total
955
- await update({
956
- tableName: 'orders',
957
- dbClient: tx,
958
- id: order.id,
959
- data: {
960
- total: totalAmount,
961
- status: 'confirmed',
962
- },
963
- })
964
-
965
- return { order, totalAmount }
966
- })
967
- }
968
- ```
969
-
970
- ### beginTransaction
971
-
972
- Creates a transaction client for manual transaction management.
973
-
974
- ```typescript
975
- const transaction = await beginTransaction(dbClient: IDatabaseClient): Promise<ITransactionClient>
976
- ```
977
-
978
- #### Examples
979
-
980
- ```typescript
981
- import {
982
- beginTransaction,
983
- insert,
984
- update,
985
- } from '@starbemtech/star-db-query-builder'
986
-
987
- // Manual transaction management
988
- const complexOperation = async () => {
989
- const transaction = await beginTransaction(dbClient)
990
-
991
- try {
992
- // First operation
993
- const user = await insert({
994
- tableName: 'users',
995
- dbClient: transaction,
996
- data: { name: 'John Doe', email: 'john@example.com' },
997
- })
998
-
999
- // Second operation
1000
- const profile = await insert({
1001
- tableName: 'user_profiles',
1002
- dbClient: transaction,
1003
- data: { user_id: user.id, bio: 'Hello world' },
1004
- })
1005
-
1006
- // Third operation
1007
- await update({
1008
- tableName: 'users',
1009
- dbClient: transaction,
1010
- id: user.id,
1011
- data: { profile_created: true },
1012
- })
1013
-
1014
- // Commit all changes
1015
- await transaction.commit()
1016
- return { user, profile }
1017
- } catch (error) {
1018
- // Rollback on any error
1019
- await transaction.rollback()
1020
- throw error
1021
- }
1022
- }
1023
- ```
1024
-
1025
- ### ITransactionClient Interface
1026
-
1027
- ```typescript
1028
- interface ITransactionClient {
1029
- query: <T>(sql: string, params?: any[]) => Promise<T>
1030
- commit: () => Promise<void>
1031
- rollback: () => Promise<void>
1032
- }
1033
- ```
1034
-
1035
- ## Types and Interfaces
1036
-
1037
- ### Conditions
1038
-
1039
- Used for building WHERE clauses with type safety.
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
-
1043
- ```typescript
1044
- type Conditions<T> = {
1045
- [P in keyof T]?: Condition<T[P]>
1046
- } & LogicalCondition<T>
1047
-
1048
- type Condition<T> = OperatorCondition | LogicalCondition<T>
1049
-
1050
- interface OperatorCondition {
1051
- operator:
1052
- | '='
1053
- | '!='
1054
- | '>'
1055
- | '<'
1056
- | '>='
1057
- | '<='
1058
- | 'LIKE'
1059
- | 'NOT LIKE'
1060
- | 'ILIKE'
1061
- | 'IN'
1062
- | 'NOT IN'
1063
- | 'BETWEEN'
1064
- | 'IS NULL'
1065
- | 'IS NOT NULL'
1066
- | 'NOT EXISTS'
1067
- value: SimpleValue | SimpleValue[]
1068
- }
1069
-
1070
- interface LogicalCondition<T> {
1071
- OR?: Conditions<T>[]
1072
- AND?: Conditions<T>[]
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>[]
1077
- notExists?: OperatorCondition
1078
- }
1079
- ```
1080
-
1081
- ### OrderBy
1082
-
1083
- Used for specifying sort order.
1084
-
1085
- ```typescript
1086
- type OrderBy = { field: string; direction: 'ASC' | 'DESC' }[]
1087
- ```
1088
-
1089
- ### JoinClause
1090
-
1091
- Used for JOIN operations.
1092
-
1093
- ```typescript
1094
- interface JoinClause {
1095
- type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL'
1096
- table: string
1097
- on: string
1098
- }
1099
- ```
1100
-
1101
- ## Advanced Usage
1102
-
1103
- ### Complex WHERE Conditions
1104
-
1105
- ```typescript
1106
- const users = await findMany({
1107
- tableName: 'users',
1108
- dbClient,
1109
- where: {
1110
- AND: [
1111
- { status: { operator: '=', value: 'active' } },
1112
- {
1113
- OR: [
1114
- { age: { operator: '>=', value: 18 } },
1115
- { verified: { operator: '=', value: true } },
1116
- ],
1117
- },
1118
- { created_at: { operator: '>=', value: new Date('2023-01-01') } },
1119
- ],
1120
- },
1121
- })
1122
- ```
1123
-
1124
- ### Using Unaccent for PostgreSQL
1125
-
1126
- ```typescript
1127
- const users = await findMany({
1128
- tableName: 'users',
1129
- dbClient,
1130
- where: {
1131
- name: { operator: 'ILIKE', value: '%joรฃo%' },
1132
- },
1133
- unaccent: true, // Enables unaccent search
1134
- })
1135
- ```
1136
-
1137
- ## Monitoring
1138
-
1139
- The library provides a comprehensive monitoring system to track database operations and performance.
1140
-
1141
- ### Monitor Events
1142
-
1143
- ```typescript
1144
- import { monitor, MonitorEvents } from '@starbemtech/star-db-query-builder'
1145
-
1146
- // Monitor connection events
1147
- monitor.on(MonitorEvents.CONNECTION_CREATED, (data) => {
1148
- console.log('Database connection created:', data)
1149
- })
1150
-
1151
- // Monitor query events
1152
- monitor.on(MonitorEvents.QUERY_START, (data) => {
1153
- console.log('Query started:', {
1154
- sql: data.sql,
1155
- params: data.params,
1156
- clientType: data.clientType,
1157
- attempt: data.attempt,
1158
- })
1159
- })
1160
-
1161
- monitor.on(MonitorEvents.QUERY_END, (data) => {
1162
- console.log('Query completed:', {
1163
- elapsedTime: data.elapsedTime,
1164
- clientType: data.clientType,
1165
- })
1166
- })
1167
-
1168
- monitor.on(MonitorEvents.QUERY_ERROR, (data) => {
1169
- console.error('Query failed:', {
1170
- error: data.error,
1171
- sql: data.sql,
1172
- elapsedTime: data.elapsedTime,
1173
- })
1174
- })
1175
-
1176
- // Monitor transaction events
1177
- monitor.on(MonitorEvents.TRANSACTION_COMMIT, (data) => {
1178
- console.log('Transaction committed:', data)
1179
- })
1180
-
1181
- monitor.on(MonitorEvents.TRANSACTION_ROLLBACK, (data) => {
1182
- console.log('Transaction rolled back:', data)
1183
- })
1184
-
1185
- // Monitor retry attempts
1186
- monitor.on(MonitorEvents.RETRY_ATTEMPT, (data) => {
1187
- console.warn('Retry attempt:', {
1188
- attempt: data.attempt,
1189
- error: data.error,
1190
- sql: data.sql,
1191
- })
1192
- })
1193
- ```
1194
-
1195
- ### Custom Monitoring Implementation
1196
-
1197
- ```typescript
1198
- // Example: Log all database operations to a file
1199
- import fs from 'fs'
1200
- import path from 'path'
1201
-
1202
- const logFile = path.join(__dirname, 'database.log')
1203
-
1204
- monitor.on(MonitorEvents.QUERY_START, (data) => {
1205
- const logEntry = {
1206
- timestamp: new Date().toISOString(),
1207
- event: 'QUERY_START',
1208
- sql: data.sql,
1209
- params: data.params,
1210
- clientType: data.clientType,
1211
- }
1212
-
1213
- fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n')
1214
- })
1215
-
1216
- monitor.on(MonitorEvents.QUERY_END, (data) => {
1217
- const logEntry = {
1218
- timestamp: new Date().toISOString(),
1219
- event: 'QUERY_END',
1220
- elapsedTime: data.elapsedTime,
1221
- clientType: data.clientType,
1222
- }
1223
-
1224
- fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n')
1225
- })
1226
- ```
1227
-
1228
- ### Performance Monitoring
1229
-
1230
- ```typescript
1231
- // Track slow queries
1232
- monitor.on(MonitorEvents.QUERY_END, (data) => {
1233
- if (data.elapsedTime > 1000) {
1234
- // Queries taking more than 1 second
1235
- console.warn('Slow query detected:', {
1236
- sql: data.sql,
1237
- elapsedTime: data.elapsedTime,
1238
- clientType: data.clientType,
1239
- })
1240
- }
1241
- })
1242
-
1243
- // Track connection pool usage
1244
- monitor.on(MonitorEvents.CONNECTION_CREATED, (data) => {
1245
- console.log('Connection pool status:', {
1246
- clientType: data.clientType,
1247
- poolOptions: data.poolOptions,
1248
- })
1249
- })
1250
- ```
1251
-
1252
- ## Best Practices
1253
-
1254
- ### 1. Use TypeScript Types
1255
-
1256
- ```typescript
1257
- interface User {
1258
- id: string
1259
- name: string
1260
- email: string
1261
- created_at: Date
1262
- }
1263
-
1264
- const users: User[] = await findMany<User>({
1265
- tableName: 'users',
1266
- dbClient,
1267
- where: { status: { operator: '=', value: 'active' } },
1268
- })
1269
- ```
1270
-
1271
- ### 2. Use Specific Field Selection
1272
-
1273
- ```typescript
1274
- // Good: Select only needed fields
1275
- const users = await findMany({
1276
- tableName: 'users',
1277
- dbClient,
1278
- select: ['id', 'name', 'email'],
1279
- where: { status: { operator: '=', value: 'active' } },
1280
- })
1281
-
1282
- // Avoid: Selecting all fields when not needed
1283
- const users = await findMany({
1284
- tableName: 'users',
1285
- dbClient,
1286
- where: { status: { operator: '=', value: 'active' } },
1287
- })
1288
- ```
1289
-
1290
- ### 3. Use Pagination for Large Datasets
1291
-
1292
- ```typescript
1293
- const users = await findMany({
1294
- tableName: 'users',
1295
- dbClient,
1296
- limit: 50,
1297
- offset: 0,
1298
- orderBy: [{ field: 'created_at', direction: 'DESC' }],
1299
- })
1300
- ```
1301
-
1302
- ### 4. Use Batch Operations When Possible
1303
-
1304
- ```typescript
1305
- // Good: Batch insert
1306
- const users = await insertMany({
1307
- tableName: 'users',
1308
- dbClient,
1309
- data: userArray,
1310
- })
1311
-
1312
- // Avoid: Multiple individual inserts
1313
- for (const user of userArray) {
1314
- await insert({ tableName: 'users', dbClient, data: user })
1315
- }
1316
- ```
1317
-
1318
- ### 5. Handle Errors Properly
1319
-
1320
- ```typescript
1321
- try {
1322
- const user = await findFirst({
1323
- tableName: 'users',
1324
- dbClient,
1325
- where: { email: { operator: '=', value: 'user@example.com' } },
1326
- })
1327
- } catch (error) {
1328
- console.error('Database error:', error.message)
1329
- // Handle error appropriately
1330
- }
1331
- ```
1332
-
1333
- ### 6. Use Raw Queries Sparingly
1334
-
1335
- ```typescript
1336
- // Use built-in methods when possible
1337
- const users = await findMany({
1338
- tableName: 'users',
1339
- dbClient,
1340
- where: { status: { operator: '=', value: 'active' } },
1341
- })
1342
-
1343
- // Use rawQuery only for complex operations
1344
- const complexStats = await rawQuery({
1345
- dbClient,
1346
- sql: 'SELECT ... complex aggregation ...',
1347
- })
1348
- ```
1349
-
1350
- ### 7. Use Transactions for Data Consistency
1351
-
1352
- ```typescript
1353
- // Good: Use transactions for related operations
1354
- const createUserWithProfile = async (userData: any, profileData: any) => {
1355
- return withTransaction(dbClient, async (tx) => {
1356
- const user = await insert({
1357
- tableName: 'users',
1358
- dbClient: tx,
1359
- data: userData,
1360
- })
1361
-
1362
- await insert({
1363
- tableName: 'user_profiles',
1364
- dbClient: tx,
1365
- data: { ...profileData, user_id: user.id },
1366
- })
1367
-
1368
- return user
1369
- })
1370
- }
1371
-
1372
- // Avoid: Multiple separate operations without transactions
1373
- const badUserCreation = async (userData: any, profileData: any) => {
1374
- const user = await insert({
1375
- tableName: 'users',
1376
- dbClient,
1377
- data: userData,
1378
- })
1379
-
1380
- // If this fails, the user will be created but profile won't
1381
- await insert({
1382
- tableName: 'user_profiles',
1383
- dbClient,
1384
- data: { ...profileData, user_id: user.id },
1385
- })
1386
-
1387
- return user
1388
- }
1389
- ```
1390
-
1391
- ### 8. Keep Transactions Short
1392
-
1393
- ```typescript
1394
- // Good: Short, focused transaction
1395
- const updateUserStatus = async (userId: string, status: string) => {
1396
- return withTransaction(dbClient, async (tx) => {
1397
- await update({
1398
- tableName: 'users',
1399
- dbClient: tx,
1400
- id: userId,
1401
- data: { status },
1402
- })
1403
-
1404
- await insert({
1405
- tableName: 'user_status_history',
1406
- dbClient: tx,
1407
- data: { user_id: userId, status, changed_at: new Date() },
1408
- })
1409
- })
1410
- }
1411
-
1412
- // Avoid: Long-running transactions
1413
- const badTransaction = async () => {
1414
- return withTransaction(dbClient, async (tx) => {
1415
- // ... many operations
1416
- await someSlowOperation() // This could timeout
1417
- // ... more operations
1418
- })
1419
- }
1420
- ```
1421
-
1422
- ## Error Handling
1423
-
1424
- The library throws descriptive errors for common issues:
1425
-
1426
- ### Common Errors
1427
-
1428
- - `Table name is required`
1429
- - `DB client is required`
1430
- - `Data object is required`
1431
- - `ID is required`
1432
- - `Where condition is required`
1433
- - `Raw query execution failed: [database message]`
1434
- - `Transaction execution failed: [database message]`
1435
-
1436
- ### Transaction Error Handling
1437
-
1438
- ```typescript
1439
- import {
1440
- withTransaction,
1441
- insert,
1442
- update,
1443
- } from '@starbemtech/star-db-query-builder'
1444
-
1445
- const safeTransaction = async () => {
1446
- try {
1447
- return await withTransaction(dbClient, async (tx) => {
1448
- // Transaction operations
1449
- const result = await someOperation(tx)
1450
- return result
1451
- })
1452
- } catch (error) {
1453
- // Transaction was automatically rolled back
1454
- console.error('Transaction failed:', error.message)
1455
-
1456
- // Handle specific error types
1457
- if (error.message.includes('deadlock detected')) {
1458
- // Handle deadlock - you might want to retry
1459
- console.warn('Deadlock detected, retrying...')
1460
- // Implement retry logic
1461
- } else if (error.message.includes('serialization failure')) {
1462
- // Handle serialization failure
1463
- console.warn('Serialization failure, retrying...')
1464
- // Implement retry logic
1465
- } else if (error.message.includes('connection lost')) {
1466
- // Handle connection issues
1467
- console.error('Database connection lost')
1468
- // Implement reconnection logic
1469
- } else {
1470
- // Handle other errors
1471
- console.error('Transaction error:', error.message)
1472
- }
1473
-
1474
- throw error
1475
- }
1476
- }
1477
- ```
1478
-
1479
- ### Retry Logic for Transient Errors
1480
-
1481
- ```typescript
1482
- const retryTransaction = async <T>(
1483
- transactionFn: (tx: ITransactionClient) => Promise<T>,
1484
- maxRetries: number = 3
1485
- ): Promise<T> => {
1486
- let lastError: Error
1487
-
1488
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
1489
- try {
1490
- return await withTransaction(dbClient, transactionFn)
1491
- } catch (error) {
1492
- lastError = error as Error
1493
-
1494
- // Check if error is retryable
1495
- if (isRetryableError(error) && attempt < maxRetries) {
1496
- const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
1497
- console.warn(
1498
- `Transaction attempt ${attempt} failed, retrying in ${delay}ms...`
1499
- )
1500
- await new Promise((resolve) => setTimeout(resolve, delay))
1501
- continue
1502
- }
1503
-
1504
- throw error
1505
- }
1506
- }
1507
-
1508
- throw lastError!
1509
- }
1510
-
1511
- const isRetryableError = (error: any): boolean => {
1512
- const retryableErrors = [
1513
- 'deadlock detected',
1514
- 'serialization failure',
1515
- 'connection lost',
1516
- 'timeout',
1517
- ]
1518
-
1519
- return retryableErrors.some((msg) =>
1520
- error.message?.toLowerCase().includes(msg)
1521
- )
1522
- }
1523
- ```
1524
-
1525
- Always wrap database operations in try-catch blocks and handle errors appropriately in your application.
137
+ Tests live alongside each module in `__tests__/` directories under `src/core`, `src/db`, and `src/monitor`.
1526
138
 
1527
139
  ## Contributing
1528
140
 
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)**.
1530
-
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)**.
1534
-
1535
- ## License
1536
-
1537
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1538
-
1539
- ## Support
141
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the branch naming, commit, and PR conventions, and [AGENTS.md](./AGENTS.md) for the full development workflow, including the mandatory SQL-identifier-sanitization rule for any function that accepts a caller-supplied table/column name.
1540
142
 
1541
- - **Documentation**: [docs/INDEX.md](docs/INDEX.md)
1542
- - **Issues**: [GitHub Issues](https://github.com/starbem/star-db-query-builder/issues)
143
+ ## CI/CD
1543
144
 
1544
- ## Changelog
145
+ `.github/workflows/ci.yml` runs on every push and pull request to `main`, across a Node 18/20/22 matrix: `pnpm install --frozen-lockfile`, then `pnpm lint`, `pnpm format:check`, `pnpm type:check`, `pnpm build`, and `pnpm test:ci`, in that order.
1545
146
 
1546
- See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history.
147
+ `.github/workflows/release.yml` runs on pushed tags matching `v*.*.*`: installs, builds, runs `test:ci`, generates release notes from git log since the previous tag, creates a GitHub Release, and publishes to npm via OIDC Trusted Publishing (no `NPM_TOKEN`).
1547
148
 
1548
- ---
149
+ ## Release Process
1549
150
 
1550
- Made with โค๏ธ by the Starbem team
151
+ See [RELEASE.md](./RELEASE.md) for the full step-by-step. In short: merge to `main`, run the local gate (lint/format/type-check/build/test:ci), update `CHANGELOG.md`, bump the version with `pnpm run version:<patch|minor|major>` (or use the already-bumped `package.json` version), then push the `vX.Y.Z` tag โ€” pushing the tag is what triggers `release.yml` and the npm publish. `scripts/release.sh` wraps the routine steps into one interactive prompt (`pnpm run release`).