@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/CHANGELOG.md +15 -0
- package/README.md +88 -1487
- package/dist/src/core/repository.d.ts +34 -2
- package/dist/src/core/repository.js +64 -19
- package/dist/src/core/repository.js.map +1 -1
- package/dist/src/core/types.d.ts +14 -6
- package/dist/src/core/utils.d.ts +17 -0
- package/dist/src/core/utils.js +83 -29
- package/dist/src/core/utils.js.map +1 -1
- package/dist/src/db/IDatabaseClient.d.ts +14 -4
- package/dist/src/db/mysqlClient.js +1 -0
- package/dist/src/db/mysqlClient.js.map +1 -1
- package/dist/src/db/pgClient.js +1 -0
- package/dist/src/db/pgClient.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,1550 +1,151 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @starbemtech/star-db-query-builder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
## Architecture
|
|
38
19
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
pnpm add @starbemtech/star-db-query-builder
|
|
87
52
|
```
|
|
88
53
|
|
|
89
|
-
|
|
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
|
|
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: '
|
|
69
|
+
name: 'default',
|
|
157
70
|
type: 'pg',
|
|
158
71
|
options: {
|
|
159
|
-
host:
|
|
72
|
+
host: process.env.DB_HOST,
|
|
160
73
|
port: 5432,
|
|
161
|
-
database:
|
|
162
|
-
user:
|
|
163
|
-
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
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
|
523
|
-
const
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
+
## Testing
|
|
670
129
|
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1542
|
-
- **Issues**: [GitHub Issues](https://github.com/starbem/star-db-query-builder/issues)
|
|
143
|
+
## CI/CD
|
|
1543
144
|
|
|
1544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`).
|