@starbemtech/star-db-query-builder 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/star-db-query-builder/SKILL.md +104 -0
- package/CHANGELOG.md +107 -55
- package/LICENSE +21 -0
- package/README.md +92 -1391
- package/bin/install-skill.js +53 -0
- package/dist/src/core/repository.d.ts +125 -10
- package/dist/src/core/repository.js +336 -43
- package/dist/src/core/repository.js.map +1 -1
- package/dist/src/core/types.d.ts +29 -6
- package/dist/src/core/utils.d.ts +119 -0
- package/dist/src/core/utils.js +353 -82
- package/dist/src/core/utils.js.map +1 -1
- package/dist/src/db/IDatabaseClient.d.ts +14 -4
- package/dist/src/db/initDb.d.ts +60 -50
- package/dist/src/db/initDb.js +96 -64
- package/dist/src/db/initDb.js.map +1 -1
- package/dist/src/db/mysqlClient.d.ts +3 -8
- package/dist/src/db/mysqlClient.js +10 -11
- package/dist/src/db/mysqlClient.js.map +1 -1
- package/dist/src/db/pgClient.js +1 -2
- package/dist/src/db/pgClient.js.map +1 -1
- package/dist/src/monitor/monitor.js +7 -0
- package/dist/src/monitor/monitor.js.map +1 -1
- package/package.json +32 -24
- package/.github/workflows/publish.yml +0 -118
- package/.prettierignore +0 -3
- package/.prettierrc +0 -5
- package/ARCHITECTURE.md +0 -313
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -131
- package/coverage/lcov-report/mysqlClient.ts.html +0 -685
- package/coverage/lcov-report/pgClient.ts.html +0 -823
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov.info +0 -533
- package/coverage/mysqlClient.ts.html +0 -685
- package/coverage/pgClient.ts.html +0 -823
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/dist/src/setupTests.d.ts +0 -26
- package/dist/src/setupTests.js +0 -43
- package/dist/src/setupTests.js.map +0 -1
- package/docs/INDEX.md +0 -145
- package/docs/methods/findFirst.md +0 -394
- package/docs/methods/findMany.md +0 -587
- package/docs/methods/insert.md +0 -536
- package/docs/methods/insertMany.md +0 -627
- package/docs/methods/joins.md +0 -781
- package/docs/methods/rawQuery.md +0 -284
- package/docs/methods/transactions.md +0 -737
- package/eslint.config.mjs +0 -77
- package/index.ts +0 -16
- package/jest.config.ts +0 -194
- package/scripts/release.sh +0 -123
- package/src/core/repository.ts +0 -865
- package/src/core/types.ts +0 -97
- package/src/core/utils.ts +0 -357
- package/src/db/IDatabaseClient.ts +0 -16
- package/src/db/__tests__/mysqlClient.test.ts +0 -262
- package/src/db/__tests__/pgClient.test.ts +0 -260
- package/src/db/initDb.ts +0 -181
- package/src/db/mysqlClient.ts +0 -200
- package/src/db/pgClient.ts +0 -246
- package/src/monitor/monitor.ts +0 -16
- package/src/setupTests.ts +0 -45
- package/tsconfig.test.json +0 -21
package/README.md
CHANGED
|
@@ -1,1450 +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
|
-
- [insert](#insert)
|
|
15
|
-
- [insertMany](#insertmany)
|
|
16
|
-
- [update](#update)
|
|
17
|
-
- [updateMany](#updatemany)
|
|
18
|
-
- [deleteOne](#deleteone)
|
|
19
|
-
- [deleteMany](#deletemany)
|
|
20
|
-
- [joins](#joins)
|
|
21
|
-
- [rawQuery](#rawquery)
|
|
22
|
-
- [Transactions](#transactions)
|
|
23
|
-
- [withTransaction](#withtransaction)
|
|
24
|
-
- [beginTransaction](#begintransaction)
|
|
25
|
-
- [Types and Interfaces](#types-and-interfaces)
|
|
26
|
-
- [Advanced Usage](#advanced-usage)
|
|
27
|
-
- [Monitoring](#monitoring)
|
|
28
|
-
- [Best Practices](#best-practices)
|
|
29
|
-
- [Error Handling](#error-handling)
|
|
30
|
-
- [Contributing](#contributing)
|
|
31
|
-
- [License](#license)
|
|
7
|
+
## Tech Stack
|
|
32
8
|
|
|
33
|
-
|
|
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
|
|
34
17
|
|
|
35
|
-
|
|
18
|
+
## Architecture
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
- **🛡️ Type Safety**: Complete TypeScript support with strong typing
|
|
39
|
-
- **⚡ Auto Retry**: Automatic retry for transient errors (timeouts, lost connections)
|
|
40
|
-
- **📊 Monitoring**: Event system for monitoring and logging
|
|
41
|
-
- **🔍 Query Builder**: Fluent interface for building complex queries
|
|
42
|
-
- **📦 Batch Operations**: Optimized batch operations (insertMany, updateMany)
|
|
20
|
+
The library is organized into three modules under `src/`:
|
|
43
21
|
|
|
44
|
-
|
|
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.
|
|
45
25
|
|
|
46
|
-
-
|
|
47
|
-
- **MySQL**: Full compatibility with MySQL 5.7+
|
|
48
|
-
- **Connection Pooling**: Efficient connection management
|
|
49
|
-
- **Transaction Support**: Full ACID transaction support with automatic rollback
|
|
50
|
-
- **Raw SQL**: Execute custom SQL queries when needed
|
|
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.
|
|
51
27
|
|
|
52
|
-
|
|
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.
|
|
53
29
|
|
|
54
|
-
|
|
55
|
-
- **Jest**: Unit and integration tests
|
|
56
|
-
- **Husky**: Git hooks for code quality
|
|
57
|
-
- **TypeScript**: Compilation and typing
|
|
30
|
+
## Security & Operational Notes
|
|
58
31
|
|
|
59
|
-
|
|
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.
|
|
34
|
+
|
|
35
|
+
## Folder Structure
|
|
36
|
+
|
|
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
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
60
49
|
|
|
61
50
|
```bash
|
|
62
|
-
npm install @starbemtech/star-db-query-builder
|
|
63
|
-
# or
|
|
64
51
|
pnpm add @starbemtech/star-db-query-builder
|
|
65
|
-
# or
|
|
66
|
-
yarn add @starbemtech/star-db-query-builder
|
|
67
52
|
```
|
|
68
53
|
|
|
69
|
-
##
|
|
54
|
+
## Usage
|
|
70
55
|
|
|
71
56
|
```typescript
|
|
72
57
|
import {
|
|
73
58
|
initDb,
|
|
74
59
|
getDbClient,
|
|
75
60
|
findFirst,
|
|
61
|
+
findMany,
|
|
76
62
|
insert,
|
|
63
|
+
update,
|
|
64
|
+
withTransaction,
|
|
77
65
|
} from '@starbemtech/star-db-query-builder'
|
|
78
66
|
|
|
79
|
-
// Initialize
|
|
80
|
-
await initDb({
|
|
81
|
-
type: 'pg', // or 'mysql'
|
|
82
|
-
options: {
|
|
83
|
-
host: 'localhost',
|
|
84
|
-
port: 5432,
|
|
85
|
-
database: 'myapp',
|
|
86
|
-
user: 'username',
|
|
87
|
-
password: 'password',
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
// Get database client
|
|
92
|
-
const dbClient = getDbClient()
|
|
93
|
-
|
|
94
|
-
// Find a user
|
|
95
|
-
const user = await findFirst({
|
|
96
|
-
tableName: 'users',
|
|
97
|
-
dbClient,
|
|
98
|
-
where: { email: { operator: '=', value: 'user@example.com' } },
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
// Insert a new user
|
|
102
|
-
const newUser = await insert({
|
|
103
|
-
tableName: 'users',
|
|
104
|
-
dbClient,
|
|
105
|
-
data: { name: 'John Doe', email: 'john@example.com' },
|
|
106
|
-
})
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Database Initialization
|
|
110
|
-
|
|
111
|
-
### initDb
|
|
112
|
-
|
|
113
|
-
Initializes a database connection with the specified configuration.
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
await initDb({
|
|
117
|
-
name?: string, // Optional client name (default: 'default')
|
|
118
|
-
type: 'pg' | 'mysql', // Database type
|
|
119
|
-
options: PoolConfig | MySqlPoolOptions, // Connection options
|
|
120
|
-
retryOptions?: RetryOptions, // Optional retry configuration
|
|
121
|
-
installUnaccentExtension?: boolean // PostgreSQL unaccent extension
|
|
122
|
-
})
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
#### PostgreSQL Example
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
import { initDb } from '@starbemtech/star-db-query-builder'
|
|
129
|
-
|
|
67
|
+
// Initialize a named PostgreSQL connection (call once at startup)
|
|
130
68
|
await initDb({
|
|
131
|
-
name: '
|
|
69
|
+
name: 'default',
|
|
132
70
|
type: 'pg',
|
|
133
71
|
options: {
|
|
134
|
-
host:
|
|
72
|
+
host: process.env.DB_HOST,
|
|
135
73
|
port: 5432,
|
|
136
|
-
database:
|
|
137
|
-
user:
|
|
138
|
-
password:
|
|
139
|
-
max: 20,
|
|
140
|
-
idleTimeoutMillis: 30000,
|
|
141
|
-
connectionTimeoutMillis: 2000,
|
|
142
|
-
},
|
|
143
|
-
retryOptions: {
|
|
144
|
-
retries: 3,
|
|
145
|
-
factor: 2,
|
|
146
|
-
minTimeout: 1000,
|
|
147
|
-
maxTimeout: 5000,
|
|
148
|
-
},
|
|
149
|
-
installUnaccentExtension: true,
|
|
150
|
-
})
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
#### MySQL Example
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
import { initDb } from '@starbemtech/star-db-query-builder'
|
|
157
|
-
|
|
158
|
-
await initDb({
|
|
159
|
-
name: 'analytics',
|
|
160
|
-
type: 'mysql',
|
|
161
|
-
options: {
|
|
162
|
-
host: 'localhost',
|
|
163
|
-
port: 3306,
|
|
164
|
-
database: 'analytics',
|
|
165
|
-
user: 'root',
|
|
166
|
-
password: 'password',
|
|
167
|
-
connectionLimit: 10,
|
|
168
|
-
acquireTimeout: 60000,
|
|
169
|
-
timeout: 60000,
|
|
170
|
-
},
|
|
171
|
-
})
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### getDbClient
|
|
175
|
-
|
|
176
|
-
Retrieves a database client by name.
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
const dbClient = getDbClient(name?: string)
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
// Get default client
|
|
184
|
-
const defaultClient = getDbClient()
|
|
185
|
-
|
|
186
|
-
// Get named client
|
|
187
|
-
const analyticsClient = getDbClient('analytics')
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
## Query Methods
|
|
191
|
-
|
|
192
|
-
### findFirst
|
|
193
|
-
|
|
194
|
-
Finds the first record that matches the specified conditions.
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
const result = await findFirst<T>({
|
|
198
|
-
tableName: string,
|
|
199
|
-
dbClient: IDatabaseClient,
|
|
200
|
-
select?: string[],
|
|
201
|
-
where?: Conditions<T>,
|
|
202
|
-
groupBy?: string[],
|
|
203
|
-
orderBy?: OrderBy
|
|
204
|
-
})
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
#### Examples
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
// Find user by email
|
|
211
|
-
const user = await findFirst({
|
|
212
|
-
tableName: 'users',
|
|
213
|
-
dbClient,
|
|
214
|
-
where: {
|
|
215
|
-
email: { operator: '=', value: 'user@example.com' },
|
|
216
|
-
},
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
// Find with specific fields
|
|
220
|
-
const user = await findFirst({
|
|
221
|
-
tableName: 'users',
|
|
222
|
-
dbClient,
|
|
223
|
-
select: ['id', 'name', 'email'],
|
|
224
|
-
where: {
|
|
225
|
-
status: { operator: '=', value: 'active' },
|
|
226
|
-
},
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
// Find with complex conditions
|
|
230
|
-
const user = await findFirst({
|
|
231
|
-
tableName: 'users',
|
|
232
|
-
dbClient,
|
|
233
|
-
where: {
|
|
234
|
-
AND: [
|
|
235
|
-
{ email: { operator: '=', value: 'user@example.com' } },
|
|
236
|
-
{ status: { operator: '=', value: 'active' } },
|
|
237
|
-
],
|
|
238
|
-
},
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// Find with ordering
|
|
242
|
-
const latestUser = await findFirst({
|
|
243
|
-
tableName: 'users',
|
|
244
|
-
dbClient,
|
|
245
|
-
orderBy: [{ field: 'created_at', direction: 'DESC' }],
|
|
246
|
-
})
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### findMany
|
|
250
|
-
|
|
251
|
-
Finds multiple records that match the specified conditions.
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
const results = await findMany<T>({
|
|
255
|
-
tableName: string,
|
|
256
|
-
dbClient: IDatabaseClient,
|
|
257
|
-
select?: string[],
|
|
258
|
-
where?: Conditions<T>,
|
|
259
|
-
groupBy?: string[],
|
|
260
|
-
orderBy?: OrderBy,
|
|
261
|
-
limit?: number,
|
|
262
|
-
offset?: number,
|
|
263
|
-
unaccent?: boolean
|
|
264
|
-
})
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
#### Examples
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
// Find all active users
|
|
271
|
-
const users = await findMany({
|
|
272
|
-
tableName: 'users',
|
|
273
|
-
dbClient,
|
|
274
|
-
where: {
|
|
275
|
-
status: { operator: '=', value: 'active' },
|
|
276
|
-
},
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
// Find with pagination
|
|
280
|
-
const users = await findMany({
|
|
281
|
-
tableName: 'users',
|
|
282
|
-
dbClient,
|
|
283
|
-
limit: 10,
|
|
284
|
-
offset: 20,
|
|
285
|
-
orderBy: [{ field: 'created_at', direction: 'DESC' }],
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// Find with complex conditions
|
|
289
|
-
const users = await findMany({
|
|
290
|
-
tableName: 'users',
|
|
291
|
-
dbClient,
|
|
292
|
-
where: {
|
|
293
|
-
OR: [
|
|
294
|
-
{ status: { operator: '=', value: 'active' } },
|
|
295
|
-
{ status: { operator: '=', value: 'pending' } },
|
|
296
|
-
],
|
|
297
|
-
created_at: {
|
|
298
|
-
operator: '>=',
|
|
299
|
-
value: new Date('2023-01-01'),
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
// Find with grouping
|
|
305
|
-
const userStats = await findMany({
|
|
306
|
-
tableName: 'users',
|
|
307
|
-
dbClient,
|
|
308
|
-
select: ['status', 'COUNT(*) as count'],
|
|
309
|
-
groupBy: ['status'],
|
|
310
|
-
})
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### insert
|
|
314
|
-
|
|
315
|
-
Inserts a single record into the database.
|
|
316
|
-
|
|
317
|
-
```typescript
|
|
318
|
-
const result = await insert<P, R>({
|
|
319
|
-
tableName: string,
|
|
320
|
-
dbClient: IDatabaseClient,
|
|
321
|
-
data: P,
|
|
322
|
-
returning?: string[]
|
|
323
|
-
})
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
#### Examples
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
// Simple insert
|
|
330
|
-
const user = await insert({
|
|
331
|
-
tableName: 'users',
|
|
332
|
-
dbClient,
|
|
333
|
-
data: {
|
|
334
|
-
name: 'John Doe',
|
|
335
|
-
email: 'john@example.com',
|
|
336
|
-
age: 30,
|
|
337
|
-
},
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
// Insert with specific returning fields
|
|
341
|
-
const user = await insert({
|
|
342
|
-
tableName: 'users',
|
|
343
|
-
dbClient,
|
|
344
|
-
data: {
|
|
345
|
-
name: 'Jane Doe',
|
|
346
|
-
email: 'jane@example.com',
|
|
347
|
-
},
|
|
348
|
-
returning: ['id', 'name', 'email', 'created_at'],
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// Insert with TypeScript typing
|
|
352
|
-
interface UserData {
|
|
353
|
-
name: string
|
|
354
|
-
email: string
|
|
355
|
-
age: number
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
interface User {
|
|
359
|
-
id: string
|
|
360
|
-
name: string
|
|
361
|
-
email: string
|
|
362
|
-
age: number
|
|
363
|
-
created_at: Date
|
|
364
|
-
updated_at: Date
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const user: User = await insert<UserData, User>({
|
|
368
|
-
tableName: 'users',
|
|
369
|
-
dbClient,
|
|
370
|
-
data: {
|
|
371
|
-
name: 'John Doe',
|
|
372
|
-
email: 'john@example.com',
|
|
373
|
-
age: 30,
|
|
374
|
-
},
|
|
375
|
-
})
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### insertMany
|
|
379
|
-
|
|
380
|
-
Inserts multiple records into the database in a single operation.
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
const results = await insertMany<P, R>({
|
|
384
|
-
tableName: string,
|
|
385
|
-
dbClient: IDatabaseClient,
|
|
386
|
-
data: P[],
|
|
387
|
-
returning?: string[]
|
|
388
|
-
})
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
#### Examples
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
// Insert multiple users
|
|
395
|
-
const users = await insertMany({
|
|
396
|
-
tableName: 'users',
|
|
397
|
-
dbClient,
|
|
398
|
-
data: [
|
|
399
|
-
{ name: 'John Doe', email: 'john@example.com' },
|
|
400
|
-
{ name: 'Jane Doe', email: 'jane@example.com' },
|
|
401
|
-
{ name: 'Bob Smith', email: 'bob@example.com' },
|
|
402
|
-
],
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
// Insert with returning fields
|
|
406
|
-
const users = await insertMany({
|
|
407
|
-
tableName: 'users',
|
|
408
|
-
dbClient,
|
|
409
|
-
data: [
|
|
410
|
-
{ name: 'John Doe', email: 'john@example.com' },
|
|
411
|
-
{ name: 'Jane Doe', email: 'jane@example.com' },
|
|
412
|
-
],
|
|
413
|
-
returning: ['id', 'name', 'email'],
|
|
414
|
-
})
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### update
|
|
418
|
-
|
|
419
|
-
Updates a single record by ID.
|
|
420
|
-
|
|
421
|
-
```typescript
|
|
422
|
-
const result = await update<P, R>({
|
|
423
|
-
tableName: string,
|
|
424
|
-
dbClient: IDatabaseClient,
|
|
425
|
-
id: string,
|
|
426
|
-
data: P,
|
|
427
|
-
returning?: string[]
|
|
428
|
-
})
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
#### Examples
|
|
432
|
-
|
|
433
|
-
```typescript
|
|
434
|
-
// Simple update
|
|
435
|
-
const updatedUser = await update({
|
|
436
|
-
tableName: 'users',
|
|
437
|
-
dbClient,
|
|
438
|
-
id: 'user-123',
|
|
439
|
-
data: {
|
|
440
|
-
name: 'John Updated',
|
|
441
|
-
age: 31,
|
|
442
|
-
},
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
// Update with returning fields
|
|
446
|
-
const updatedUser = await update({
|
|
447
|
-
tableName: 'users',
|
|
448
|
-
dbClient,
|
|
449
|
-
id: 'user-123',
|
|
450
|
-
data: {
|
|
451
|
-
status: 'active',
|
|
452
|
-
last_login: new Date(),
|
|
453
|
-
},
|
|
454
|
-
returning: ['id', 'status', 'last_login', 'updated_at'],
|
|
455
|
-
})
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
### updateMany
|
|
459
|
-
|
|
460
|
-
Updates multiple records based on specified conditions.
|
|
461
|
-
|
|
462
|
-
```typescript
|
|
463
|
-
const results = await updateMany<P, R>({
|
|
464
|
-
tableName: string,
|
|
465
|
-
dbClient: IDatabaseClient,
|
|
466
|
-
data: P,
|
|
467
|
-
where: Conditions<T>,
|
|
468
|
-
returning?: string[]
|
|
469
|
-
})
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
#### Examples
|
|
473
|
-
|
|
474
|
-
```typescript
|
|
475
|
-
// Update all inactive users
|
|
476
|
-
const updatedUsers = await updateMany({
|
|
477
|
-
tableName: 'users',
|
|
478
|
-
dbClient,
|
|
479
|
-
data: {
|
|
480
|
-
status: 'active',
|
|
481
|
-
updated_at: new Date(),
|
|
482
|
-
},
|
|
483
|
-
where: {
|
|
484
|
-
status: { operator: '=', value: 'inactive' },
|
|
485
|
-
},
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
// Update with complex conditions
|
|
489
|
-
const updatedUsers = await updateMany({
|
|
490
|
-
tableName: 'users',
|
|
491
|
-
dbClient,
|
|
492
|
-
data: {
|
|
493
|
-
last_login: new Date(),
|
|
494
|
-
login_count: { operator: '+', value: 1 },
|
|
495
|
-
},
|
|
496
|
-
where: {
|
|
497
|
-
AND: [
|
|
498
|
-
{ status: { operator: '=', value: 'active' } },
|
|
499
|
-
{ last_login: { operator: '<', value: new Date('2023-01-01') } },
|
|
500
|
-
],
|
|
501
|
-
},
|
|
502
|
-
returning: ['id', 'name', 'last_login', 'login_count'],
|
|
503
|
-
})
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### deleteOne
|
|
507
|
-
|
|
508
|
-
Deletes a single record by ID (soft delete by default).
|
|
509
|
-
|
|
510
|
-
```typescript
|
|
511
|
-
await deleteOne<T>({
|
|
512
|
-
tableName: string,
|
|
513
|
-
dbClient: IDatabaseClient,
|
|
514
|
-
id: string,
|
|
515
|
-
permanently?: boolean
|
|
516
|
-
})
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
#### Examples
|
|
520
|
-
|
|
521
|
-
```typescript
|
|
522
|
-
// Soft delete (sets status to 'deleted')
|
|
523
|
-
await deleteOne({
|
|
524
|
-
tableName: 'users',
|
|
525
|
-
dbClient,
|
|
526
|
-
id: 'user-123',
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
// Permanent delete
|
|
530
|
-
await deleteOne({
|
|
531
|
-
tableName: 'users',
|
|
532
|
-
dbClient,
|
|
533
|
-
id: 'user-123',
|
|
534
|
-
permanently: true,
|
|
535
|
-
})
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
### deleteMany
|
|
539
|
-
|
|
540
|
-
Deletes multiple records by IDs (soft delete by default).
|
|
541
|
-
|
|
542
|
-
```typescript
|
|
543
|
-
await deleteMany<T>({
|
|
544
|
-
tableName: string,
|
|
545
|
-
dbClient: IDatabaseClient,
|
|
546
|
-
ids: string[] | number[],
|
|
547
|
-
field?: string,
|
|
548
|
-
permanently?: boolean
|
|
549
|
-
})
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
#### Examples
|
|
553
|
-
|
|
554
|
-
```typescript
|
|
555
|
-
// Soft delete multiple users
|
|
556
|
-
await deleteMany({
|
|
557
|
-
tableName: 'users',
|
|
558
|
-
dbClient,
|
|
559
|
-
ids: ['user-1', 'user-2', 'user-3'],
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
// Permanent delete with custom field
|
|
563
|
-
await deleteMany({
|
|
564
|
-
tableName: 'orders',
|
|
565
|
-
dbClient,
|
|
566
|
-
ids: [1, 2, 3],
|
|
567
|
-
field: 'order_id',
|
|
568
|
-
permanently: true,
|
|
569
|
-
})
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
### joins
|
|
573
|
-
|
|
574
|
-
Executes queries with JOIN operations.
|
|
575
|
-
|
|
576
|
-
```typescript
|
|
577
|
-
const results = await joins<T>({
|
|
578
|
-
tableName: string,
|
|
579
|
-
dbClient: IDatabaseClient,
|
|
580
|
-
select: string[],
|
|
581
|
-
joins: JoinClause[],
|
|
582
|
-
where?: Conditions<T>,
|
|
583
|
-
groupBy?: string[],
|
|
584
|
-
orderBy?: OrderBy,
|
|
585
|
-
limit?: number,
|
|
586
|
-
offset?: number,
|
|
587
|
-
unaccent?: boolean
|
|
588
|
-
})
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
#### Examples
|
|
592
|
-
|
|
593
|
-
```typescript
|
|
594
|
-
// Simple JOIN
|
|
595
|
-
const usersWithOrders = await joins({
|
|
596
|
-
tableName: 'users',
|
|
597
|
-
dbClient,
|
|
598
|
-
select: ['users.id', 'users.name', 'orders.total'],
|
|
599
|
-
joins: [
|
|
600
|
-
{
|
|
601
|
-
type: 'LEFT',
|
|
602
|
-
table: 'orders',
|
|
603
|
-
on: 'users.id = orders.user_id',
|
|
604
|
-
},
|
|
605
|
-
],
|
|
606
|
-
where: {
|
|
607
|
-
'users.status': { operator: '=', value: 'active' },
|
|
608
|
-
},
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
// Multiple JOINs
|
|
612
|
-
const report = await joins({
|
|
613
|
-
tableName: 'users',
|
|
614
|
-
dbClient,
|
|
615
|
-
select: [
|
|
616
|
-
'users.name',
|
|
617
|
-
'users.email',
|
|
618
|
-
'COUNT(orders.id) as order_count',
|
|
619
|
-
'SUM(orders.total) as total_spent',
|
|
620
|
-
'plans.name as plan_name',
|
|
621
|
-
],
|
|
622
|
-
joins: [
|
|
623
|
-
{
|
|
624
|
-
type: 'LEFT',
|
|
625
|
-
table: 'orders',
|
|
626
|
-
on: 'users.id = orders.user_id',
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
type: 'LEFT',
|
|
630
|
-
table: 'user_plans',
|
|
631
|
-
on: 'users.id = user_plans.user_id',
|
|
632
|
-
},
|
|
633
|
-
{
|
|
634
|
-
type: 'LEFT',
|
|
635
|
-
table: 'plans',
|
|
636
|
-
on: 'user_plans.plan_id = plans.id',
|
|
637
|
-
},
|
|
638
|
-
],
|
|
639
|
-
groupBy: ['users.id', 'users.name', 'users.email', 'plans.name'],
|
|
640
|
-
having: {
|
|
641
|
-
'COUNT(orders.id)': { operator: '>', value: 0 },
|
|
642
|
-
},
|
|
643
|
-
orderBy: [{ field: 'total_spent', direction: 'DESC' }],
|
|
644
|
-
})
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### rawQuery
|
|
648
|
-
|
|
649
|
-
Executes raw SQL queries directly on the database.
|
|
650
|
-
|
|
651
|
-
```typescript
|
|
652
|
-
const result = await rawQuery<T>({
|
|
653
|
-
dbClient: IDatabaseClient,
|
|
654
|
-
sql: string,
|
|
655
|
-
params?: any[]
|
|
656
|
-
})
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
#### Examples
|
|
660
|
-
|
|
661
|
-
```typescript
|
|
662
|
-
// Simple raw query
|
|
663
|
-
const users = await rawQuery({
|
|
664
|
-
dbClient,
|
|
665
|
-
sql: 'SELECT * FROM users WHERE active = true',
|
|
666
|
-
})
|
|
667
|
-
|
|
668
|
-
// Raw query with parameters
|
|
669
|
-
const user = await rawQuery({
|
|
670
|
-
dbClient,
|
|
671
|
-
sql: 'SELECT * FROM users WHERE id = ? AND email = ?',
|
|
672
|
-
params: ['user-123', 'user@example.com'],
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
// Complex aggregation
|
|
676
|
-
const stats = await rawQuery({
|
|
677
|
-
dbClient,
|
|
678
|
-
sql: `
|
|
679
|
-
SELECT
|
|
680
|
-
COUNT(*) as total_users,
|
|
681
|
-
AVG(age) as avg_age,
|
|
682
|
-
MAX(created_at) as last_created
|
|
683
|
-
FROM users
|
|
684
|
-
WHERE created_at >= ?
|
|
685
|
-
`,
|
|
686
|
-
params: [new Date('2023-01-01')],
|
|
687
|
-
})
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
## Transactions
|
|
691
|
-
|
|
692
|
-
Execute multiple database operations within a single transaction to ensure data consistency and atomicity.
|
|
693
|
-
|
|
694
|
-
### withTransaction
|
|
695
|
-
|
|
696
|
-
Executes a function within a database transaction with automatic commit/rollback handling.
|
|
697
|
-
|
|
698
|
-
```typescript
|
|
699
|
-
const result = await withTransaction<T>(
|
|
700
|
-
dbClient: IDatabaseClient,
|
|
701
|
-
transactionFn: (tx: ITransactionClient) => Promise<T>
|
|
702
|
-
): Promise<T>
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
#### Examples
|
|
706
|
-
|
|
707
|
-
```typescript
|
|
708
|
-
import {
|
|
709
|
-
withTransaction,
|
|
710
|
-
insert,
|
|
711
|
-
update,
|
|
712
|
-
} from '@starbemtech/star-db-query-builder'
|
|
713
|
-
|
|
714
|
-
// Create user with profile in a single transaction
|
|
715
|
-
const createUserWithProfile = async (userData: any, profileData: any) => {
|
|
716
|
-
return withTransaction(dbClient, async (tx) => {
|
|
717
|
-
// Create user
|
|
718
|
-
const user = await insert({
|
|
719
|
-
tableName: 'users',
|
|
720
|
-
dbClient: tx,
|
|
721
|
-
data: userData,
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
// Create user profile
|
|
725
|
-
const profile = await insert({
|
|
726
|
-
tableName: 'user_profiles',
|
|
727
|
-
dbClient: tx,
|
|
728
|
-
data: {
|
|
729
|
-
...profileData,
|
|
730
|
-
user_id: user.id,
|
|
731
|
-
},
|
|
732
|
-
})
|
|
733
|
-
|
|
734
|
-
return { user, profile }
|
|
735
|
-
})
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// E-commerce order processing
|
|
739
|
-
const processOrder = async (orderData: any, orderItems: any[]) => {
|
|
740
|
-
return withTransaction(dbClient, async (tx) => {
|
|
741
|
-
// Create order
|
|
742
|
-
const order = await insert({
|
|
743
|
-
tableName: 'orders',
|
|
744
|
-
dbClient: tx,
|
|
745
|
-
data: {
|
|
746
|
-
...orderData,
|
|
747
|
-
status: 'pending',
|
|
748
|
-
total: 0,
|
|
749
|
-
},
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
let totalAmount = 0
|
|
753
|
-
|
|
754
|
-
// Create order items and calculate total
|
|
755
|
-
for (const item of orderItems) {
|
|
756
|
-
await insert({
|
|
757
|
-
tableName: 'order_items',
|
|
758
|
-
dbClient: tx,
|
|
759
|
-
data: {
|
|
760
|
-
...item,
|
|
761
|
-
order_id: order.id,
|
|
762
|
-
},
|
|
763
|
-
})
|
|
764
|
-
|
|
765
|
-
totalAmount += item.price * item.quantity
|
|
766
|
-
|
|
767
|
-
// Update product stock
|
|
768
|
-
await update({
|
|
769
|
-
tableName: 'products',
|
|
770
|
-
dbClient: tx,
|
|
771
|
-
id: item.product_id,
|
|
772
|
-
data: {
|
|
773
|
-
stock: { operator: '-', value: item.quantity },
|
|
774
|
-
},
|
|
775
|
-
})
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Update order total
|
|
779
|
-
await update({
|
|
780
|
-
tableName: 'orders',
|
|
781
|
-
dbClient: tx,
|
|
782
|
-
id: order.id,
|
|
783
|
-
data: {
|
|
784
|
-
total: totalAmount,
|
|
785
|
-
status: 'confirmed',
|
|
786
|
-
},
|
|
787
|
-
})
|
|
788
|
-
|
|
789
|
-
return { order, totalAmount }
|
|
790
|
-
})
|
|
791
|
-
}
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
### beginTransaction
|
|
795
|
-
|
|
796
|
-
Creates a transaction client for manual transaction management.
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
const transaction = await beginTransaction(dbClient: IDatabaseClient): Promise<ITransactionClient>
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
#### Examples
|
|
803
|
-
|
|
804
|
-
```typescript
|
|
805
|
-
import {
|
|
806
|
-
beginTransaction,
|
|
807
|
-
insert,
|
|
808
|
-
update,
|
|
809
|
-
} from '@starbemtech/star-db-query-builder'
|
|
810
|
-
|
|
811
|
-
// Manual transaction management
|
|
812
|
-
const complexOperation = async () => {
|
|
813
|
-
const transaction = await beginTransaction(dbClient)
|
|
814
|
-
|
|
815
|
-
try {
|
|
816
|
-
// First operation
|
|
817
|
-
const user = await insert({
|
|
818
|
-
tableName: 'users',
|
|
819
|
-
dbClient: transaction,
|
|
820
|
-
data: { name: 'John Doe', email: 'john@example.com' },
|
|
821
|
-
})
|
|
822
|
-
|
|
823
|
-
// Second operation
|
|
824
|
-
const profile = await insert({
|
|
825
|
-
tableName: 'user_profiles',
|
|
826
|
-
dbClient: transaction,
|
|
827
|
-
data: { user_id: user.id, bio: 'Hello world' },
|
|
828
|
-
})
|
|
829
|
-
|
|
830
|
-
// Third operation
|
|
831
|
-
await update({
|
|
832
|
-
tableName: 'users',
|
|
833
|
-
dbClient: transaction,
|
|
834
|
-
id: user.id,
|
|
835
|
-
data: { profile_created: true },
|
|
836
|
-
})
|
|
837
|
-
|
|
838
|
-
// Commit all changes
|
|
839
|
-
await transaction.commit()
|
|
840
|
-
return { user, profile }
|
|
841
|
-
} catch (error) {
|
|
842
|
-
// Rollback on any error
|
|
843
|
-
await transaction.rollback()
|
|
844
|
-
throw error
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
### ITransactionClient Interface
|
|
850
|
-
|
|
851
|
-
```typescript
|
|
852
|
-
interface ITransactionClient {
|
|
853
|
-
query: <T>(sql: string, params?: any[]) => Promise<T>
|
|
854
|
-
commit: () => Promise<void>
|
|
855
|
-
rollback: () => Promise<void>
|
|
856
|
-
}
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
## Types and Interfaces
|
|
860
|
-
|
|
861
|
-
### Conditions
|
|
862
|
-
|
|
863
|
-
Used for building WHERE clauses with type safety.
|
|
864
|
-
|
|
865
|
-
```typescript
|
|
866
|
-
type Conditions<T> = {
|
|
867
|
-
[P in keyof T]?: Condition<T[P]>
|
|
868
|
-
} & LogicalCondition<T>
|
|
869
|
-
|
|
870
|
-
type Condition<T> = OperatorCondition | LogicalCondition<T>
|
|
871
|
-
|
|
872
|
-
interface OperatorCondition {
|
|
873
|
-
operator:
|
|
874
|
-
| '='
|
|
875
|
-
| '!='
|
|
876
|
-
| '>'
|
|
877
|
-
| '<'
|
|
878
|
-
| '>='
|
|
879
|
-
| '<='
|
|
880
|
-
| 'LIKE'
|
|
881
|
-
| 'NOT LIKE'
|
|
882
|
-
| 'ILIKE'
|
|
883
|
-
| 'IN'
|
|
884
|
-
| 'NOT IN'
|
|
885
|
-
| 'BETWEEN'
|
|
886
|
-
| 'IS NULL'
|
|
887
|
-
| 'IS NOT NULL'
|
|
888
|
-
| 'NOT EXISTS'
|
|
889
|
-
value: SimpleValue | SimpleValue[]
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
interface LogicalCondition<T> {
|
|
893
|
-
OR?: Conditions<T>[]
|
|
894
|
-
AND?: Conditions<T>[]
|
|
895
|
-
JOINS?: Conditions<object>
|
|
896
|
-
notExists?: OperatorCondition
|
|
897
|
-
}
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
### OrderBy
|
|
901
|
-
|
|
902
|
-
Used for specifying sort order.
|
|
903
|
-
|
|
904
|
-
```typescript
|
|
905
|
-
type OrderBy = { field: string; direction: 'ASC' | 'DESC' }[]
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
### JoinClause
|
|
909
|
-
|
|
910
|
-
Used for JOIN operations.
|
|
911
|
-
|
|
912
|
-
```typescript
|
|
913
|
-
interface JoinClause {
|
|
914
|
-
type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL'
|
|
915
|
-
table: string
|
|
916
|
-
on: string
|
|
917
|
-
}
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
## Advanced Usage
|
|
921
|
-
|
|
922
|
-
### Complex WHERE Conditions
|
|
923
|
-
|
|
924
|
-
```typescript
|
|
925
|
-
const users = await findMany({
|
|
926
|
-
tableName: 'users',
|
|
927
|
-
dbClient,
|
|
928
|
-
where: {
|
|
929
|
-
AND: [
|
|
930
|
-
{ status: { operator: '=', value: 'active' } },
|
|
931
|
-
{
|
|
932
|
-
OR: [
|
|
933
|
-
{ age: { operator: '>=', value: 18 } },
|
|
934
|
-
{ verified: { operator: '=', value: true } },
|
|
935
|
-
],
|
|
936
|
-
},
|
|
937
|
-
{ created_at: { operator: '>=', value: new Date('2023-01-01') } },
|
|
938
|
-
],
|
|
939
|
-
},
|
|
940
|
-
})
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
### Using Unaccent for PostgreSQL
|
|
944
|
-
|
|
945
|
-
```typescript
|
|
946
|
-
const users = await findMany({
|
|
947
|
-
tableName: 'users',
|
|
948
|
-
dbClient,
|
|
949
|
-
where: {
|
|
950
|
-
name: { operator: 'ILIKE', value: '%joão%' },
|
|
74
|
+
database: process.env.DB_NAME,
|
|
75
|
+
user: process.env.DB_USER,
|
|
76
|
+
password: process.env.DB_PASSWORD,
|
|
951
77
|
},
|
|
952
|
-
|
|
78
|
+
retryOptions: { retries: 3 },
|
|
953
79
|
})
|
|
954
|
-
```
|
|
955
80
|
|
|
956
|
-
|
|
81
|
+
const dbClient = getDbClient('default')
|
|
957
82
|
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
tableName: 'users',
|
|
961
|
-
dbClient,
|
|
962
|
-
where: {
|
|
963
|
-
name: { operator: 'ILIKE', value: '%joão%' },
|
|
964
|
-
},
|
|
965
|
-
unaccent: true, // Enables unaccent search
|
|
966
|
-
})
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
## Monitoring
|
|
970
|
-
|
|
971
|
-
The library provides a comprehensive monitoring system to track database operations and performance.
|
|
972
|
-
|
|
973
|
-
### Monitor Events
|
|
974
|
-
|
|
975
|
-
```typescript
|
|
976
|
-
import { monitor, MonitorEvents } from '@starbemtech/star-db-query-builder'
|
|
977
|
-
|
|
978
|
-
// Monitor connection events
|
|
979
|
-
monitor.on(MonitorEvents.CONNECTION_CREATED, (data) => {
|
|
980
|
-
console.log('Database connection created:', data)
|
|
981
|
-
})
|
|
982
|
-
|
|
983
|
-
// Monitor query events
|
|
984
|
-
monitor.on(MonitorEvents.QUERY_START, (data) => {
|
|
985
|
-
console.log('Query started:', {
|
|
986
|
-
sql: data.sql,
|
|
987
|
-
params: data.params,
|
|
988
|
-
clientType: data.clientType,
|
|
989
|
-
attempt: data.attempt,
|
|
990
|
-
})
|
|
991
|
-
})
|
|
992
|
-
|
|
993
|
-
monitor.on(MonitorEvents.QUERY_END, (data) => {
|
|
994
|
-
console.log('Query completed:', {
|
|
995
|
-
elapsedTime: data.elapsedTime,
|
|
996
|
-
clientType: data.clientType,
|
|
997
|
-
})
|
|
998
|
-
})
|
|
999
|
-
|
|
1000
|
-
monitor.on(MonitorEvents.QUERY_ERROR, (data) => {
|
|
1001
|
-
console.error('Query failed:', {
|
|
1002
|
-
error: data.error,
|
|
1003
|
-
sql: data.sql,
|
|
1004
|
-
elapsedTime: data.elapsedTime,
|
|
1005
|
-
})
|
|
1006
|
-
})
|
|
1007
|
-
|
|
1008
|
-
// Monitor transaction events
|
|
1009
|
-
monitor.on(MonitorEvents.TRANSACTION_COMMIT, (data) => {
|
|
1010
|
-
console.log('Transaction committed:', data)
|
|
1011
|
-
})
|
|
1012
|
-
|
|
1013
|
-
monitor.on(MonitorEvents.TRANSACTION_ROLLBACK, (data) => {
|
|
1014
|
-
console.log('Transaction rolled back:', data)
|
|
1015
|
-
})
|
|
1016
|
-
|
|
1017
|
-
// Monitor retry attempts
|
|
1018
|
-
monitor.on(MonitorEvents.RETRY_ATTEMPT, (data) => {
|
|
1019
|
-
console.warn('Retry attempt:', {
|
|
1020
|
-
attempt: data.attempt,
|
|
1021
|
-
error: data.error,
|
|
1022
|
-
sql: data.sql,
|
|
1023
|
-
})
|
|
1024
|
-
})
|
|
1025
|
-
```
|
|
1026
|
-
|
|
1027
|
-
### Custom Monitoring Implementation
|
|
1028
|
-
|
|
1029
|
-
```typescript
|
|
1030
|
-
// Example: Log all database operations to a file
|
|
1031
|
-
import fs from 'fs'
|
|
1032
|
-
import path from 'path'
|
|
1033
|
-
|
|
1034
|
-
const logFile = path.join(__dirname, 'database.log')
|
|
1035
|
-
|
|
1036
|
-
monitor.on(MonitorEvents.QUERY_START, (data) => {
|
|
1037
|
-
const logEntry = {
|
|
1038
|
-
timestamp: new Date().toISOString(),
|
|
1039
|
-
event: 'QUERY_START',
|
|
1040
|
-
sql: data.sql,
|
|
1041
|
-
params: data.params,
|
|
1042
|
-
clientType: data.clientType,
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n')
|
|
1046
|
-
})
|
|
1047
|
-
|
|
1048
|
-
monitor.on(MonitorEvents.QUERY_END, (data) => {
|
|
1049
|
-
const logEntry = {
|
|
1050
|
-
timestamp: new Date().toISOString(),
|
|
1051
|
-
event: 'QUERY_END',
|
|
1052
|
-
elapsedTime: data.elapsedTime,
|
|
1053
|
-
clientType: data.clientType,
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n')
|
|
1057
|
-
})
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
### Performance Monitoring
|
|
1061
|
-
|
|
1062
|
-
```typescript
|
|
1063
|
-
// Track slow queries
|
|
1064
|
-
monitor.on(MonitorEvents.QUERY_END, (data) => {
|
|
1065
|
-
if (data.elapsedTime > 1000) {
|
|
1066
|
-
// Queries taking more than 1 second
|
|
1067
|
-
console.warn('Slow query detected:', {
|
|
1068
|
-
sql: data.sql,
|
|
1069
|
-
elapsedTime: data.elapsedTime,
|
|
1070
|
-
clientType: data.clientType,
|
|
1071
|
-
})
|
|
1072
|
-
}
|
|
1073
|
-
})
|
|
1074
|
-
|
|
1075
|
-
// Track connection pool usage
|
|
1076
|
-
monitor.on(MonitorEvents.CONNECTION_CREATED, (data) => {
|
|
1077
|
-
console.log('Connection pool status:', {
|
|
1078
|
-
clientType: data.clientType,
|
|
1079
|
-
poolOptions: data.poolOptions,
|
|
1080
|
-
})
|
|
1081
|
-
})
|
|
1082
|
-
```
|
|
1083
|
-
|
|
1084
|
-
## Best Practices
|
|
1085
|
-
|
|
1086
|
-
### 1. Use TypeScript Types
|
|
1087
|
-
|
|
1088
|
-
```typescript
|
|
1089
|
-
interface User {
|
|
1090
|
-
id: string
|
|
1091
|
-
name: string
|
|
1092
|
-
email: string
|
|
1093
|
-
created_at: Date
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const users: User[] = await findMany<User>({
|
|
1097
|
-
tableName: 'users',
|
|
1098
|
-
dbClient,
|
|
1099
|
-
where: { status: { operator: '=', value: 'active' } },
|
|
1100
|
-
})
|
|
1101
|
-
```
|
|
1102
|
-
|
|
1103
|
-
### 2. Use Specific Field Selection
|
|
1104
|
-
|
|
1105
|
-
```typescript
|
|
1106
|
-
// Good: Select only needed fields
|
|
1107
|
-
const users = await findMany({
|
|
1108
|
-
tableName: 'users',
|
|
1109
|
-
dbClient,
|
|
1110
|
-
select: ['id', 'name', 'email'],
|
|
1111
|
-
where: { status: { operator: '=', value: 'active' } },
|
|
1112
|
-
})
|
|
1113
|
-
|
|
1114
|
-
// Avoid: Selecting all fields when not needed
|
|
1115
|
-
const users = await findMany({
|
|
83
|
+
// Query
|
|
84
|
+
const user = await findFirst<{ id: string; name: string }>({
|
|
1116
85
|
tableName: 'users',
|
|
1117
86
|
dbClient,
|
|
87
|
+
select: ['id', 'name'],
|
|
1118
88
|
where: { status: { operator: '=', value: 'active' } },
|
|
1119
89
|
})
|
|
1120
|
-
```
|
|
1121
|
-
|
|
1122
|
-
### 3. Use Pagination for Large Datasets
|
|
1123
90
|
|
|
1124
|
-
|
|
1125
|
-
const
|
|
91
|
+
// Insert
|
|
92
|
+
const created = await insert({
|
|
1126
93
|
tableName: 'users',
|
|
1127
94
|
dbClient,
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
orderBy: [{ field: 'created_at', direction: 'DESC' }],
|
|
1131
|
-
})
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
### 4. Use Batch Operations When Possible
|
|
1135
|
-
|
|
1136
|
-
```typescript
|
|
1137
|
-
// Good: Batch insert
|
|
1138
|
-
const users = await insertMany({
|
|
1139
|
-
tableName: 'users',
|
|
1140
|
-
dbClient,
|
|
1141
|
-
data: userArray,
|
|
1142
|
-
})
|
|
1143
|
-
|
|
1144
|
-
// Avoid: Multiple individual inserts
|
|
1145
|
-
for (const user of userArray) {
|
|
1146
|
-
await insert({ tableName: 'users', dbClient, data: user })
|
|
1147
|
-
}
|
|
1148
|
-
```
|
|
1149
|
-
|
|
1150
|
-
### 5. Handle Errors Properly
|
|
1151
|
-
|
|
1152
|
-
```typescript
|
|
1153
|
-
try {
|
|
1154
|
-
const user = await findFirst({
|
|
1155
|
-
tableName: 'users',
|
|
1156
|
-
dbClient,
|
|
1157
|
-
where: { email: { operator: '=', value: 'user@example.com' } },
|
|
1158
|
-
})
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
console.error('Database error:', error.message)
|
|
1161
|
-
// Handle error appropriately
|
|
1162
|
-
}
|
|
1163
|
-
```
|
|
1164
|
-
|
|
1165
|
-
### 6. Use Raw Queries Sparingly
|
|
1166
|
-
|
|
1167
|
-
```typescript
|
|
1168
|
-
// Use built-in methods when possible
|
|
1169
|
-
const users = await findMany({
|
|
1170
|
-
tableName: 'users',
|
|
1171
|
-
dbClient,
|
|
1172
|
-
where: { status: { operator: '=', value: 'active' } },
|
|
95
|
+
data: { name: 'John Doe', email: 'john.doe@example.com' },
|
|
96
|
+
returning: ['id', 'name', 'email'],
|
|
1173
97
|
})
|
|
1174
98
|
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
dbClient,
|
|
1178
|
-
|
|
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 } })
|
|
1179
103
|
})
|
|
1180
104
|
```
|
|
1181
105
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
```typescript
|
|
1185
|
-
// Good: Use transactions for related operations
|
|
1186
|
-
const createUserWithProfile = async (userData: any, profileData: any) => {
|
|
1187
|
-
return withTransaction(dbClient, async (tx) => {
|
|
1188
|
-
const user = await insert({
|
|
1189
|
-
tableName: 'users',
|
|
1190
|
-
dbClient: tx,
|
|
1191
|
-
data: userData,
|
|
1192
|
-
})
|
|
1193
|
-
|
|
1194
|
-
await insert({
|
|
1195
|
-
tableName: 'user_profiles',
|
|
1196
|
-
dbClient: tx,
|
|
1197
|
-
data: { ...profileData, user_id: user.id },
|
|
1198
|
-
})
|
|
1199
|
-
|
|
1200
|
-
return user
|
|
1201
|
-
})
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Avoid: Multiple separate operations without transactions
|
|
1205
|
-
const badUserCreation = async (userData: any, profileData: any) => {
|
|
1206
|
-
const user = await insert({
|
|
1207
|
-
tableName: 'users',
|
|
1208
|
-
dbClient,
|
|
1209
|
-
data: userData,
|
|
1210
|
-
})
|
|
1211
|
-
|
|
1212
|
-
// If this fails, the user will be created but profile won't
|
|
1213
|
-
await insert({
|
|
1214
|
-
tableName: 'user_profiles',
|
|
1215
|
-
dbClient,
|
|
1216
|
-
data: { ...profileData, user_id: user.id },
|
|
1217
|
-
})
|
|
1218
|
-
|
|
1219
|
-
return user
|
|
1220
|
-
}
|
|
1221
|
-
```
|
|
1222
|
-
|
|
1223
|
-
### 8. Keep Transactions Short
|
|
1224
|
-
|
|
1225
|
-
```typescript
|
|
1226
|
-
// Good: Short, focused transaction
|
|
1227
|
-
const updateUserStatus = async (userId: string, status: string) => {
|
|
1228
|
-
return withTransaction(dbClient, async (tx) => {
|
|
1229
|
-
await update({
|
|
1230
|
-
tableName: 'users',
|
|
1231
|
-
dbClient: tx,
|
|
1232
|
-
id: userId,
|
|
1233
|
-
data: { status },
|
|
1234
|
-
})
|
|
1235
|
-
|
|
1236
|
-
await insert({
|
|
1237
|
-
tableName: 'user_status_history',
|
|
1238
|
-
dbClient: tx,
|
|
1239
|
-
data: { user_id: userId, status, changed_at: new Date() },
|
|
1240
|
-
})
|
|
1241
|
-
})
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// Avoid: Long-running transactions
|
|
1245
|
-
const badTransaction = async () => {
|
|
1246
|
-
return withTransaction(dbClient, async (tx) => {
|
|
1247
|
-
// ... many operations
|
|
1248
|
-
await someSlowOperation() // This could timeout
|
|
1249
|
-
// ... more operations
|
|
1250
|
-
})
|
|
1251
|
-
}
|
|
1252
|
-
```
|
|
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/`.
|
|
1253
107
|
|
|
1254
|
-
##
|
|
108
|
+
## Available Scripts
|
|
1255
109
|
|
|
1256
|
-
|
|
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` |
|
|
1257
127
|
|
|
1258
|
-
|
|
128
|
+
## Testing
|
|
1259
129
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
- `Raw query execution failed: [database message]`
|
|
1266
|
-
- `Transaction execution failed: [database message]`
|
|
1267
|
-
|
|
1268
|
-
### Transaction Error Handling
|
|
1269
|
-
|
|
1270
|
-
```typescript
|
|
1271
|
-
import {
|
|
1272
|
-
withTransaction,
|
|
1273
|
-
insert,
|
|
1274
|
-
update,
|
|
1275
|
-
} from '@starbemtech/star-db-query-builder'
|
|
1276
|
-
|
|
1277
|
-
const safeTransaction = async () => {
|
|
1278
|
-
try {
|
|
1279
|
-
return await withTransaction(dbClient, async (tx) => {
|
|
1280
|
-
// Transaction operations
|
|
1281
|
-
const result = await someOperation(tx)
|
|
1282
|
-
return result
|
|
1283
|
-
})
|
|
1284
|
-
} catch (error) {
|
|
1285
|
-
// Transaction was automatically rolled back
|
|
1286
|
-
console.error('Transaction failed:', error.message)
|
|
1287
|
-
|
|
1288
|
-
// Handle specific error types
|
|
1289
|
-
if (error.message.includes('deadlock detected')) {
|
|
1290
|
-
// Handle deadlock - you might want to retry
|
|
1291
|
-
console.warn('Deadlock detected, retrying...')
|
|
1292
|
-
// Implement retry logic
|
|
1293
|
-
} else if (error.message.includes('serialization failure')) {
|
|
1294
|
-
// Handle serialization failure
|
|
1295
|
-
console.warn('Serialization failure, retrying...')
|
|
1296
|
-
// Implement retry logic
|
|
1297
|
-
} else if (error.message.includes('connection lost')) {
|
|
1298
|
-
// Handle connection issues
|
|
1299
|
-
console.error('Database connection lost')
|
|
1300
|
-
// Implement reconnection logic
|
|
1301
|
-
} else {
|
|
1302
|
-
// Handle other errors
|
|
1303
|
-
console.error('Transaction error:', error.message)
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
throw error
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
```
|
|
1310
|
-
|
|
1311
|
-
### Retry Logic for Transient Errors
|
|
1312
|
-
|
|
1313
|
-
```typescript
|
|
1314
|
-
const retryTransaction = async <T>(
|
|
1315
|
-
transactionFn: (tx: ITransactionClient) => Promise<T>,
|
|
1316
|
-
maxRetries: number = 3
|
|
1317
|
-
): Promise<T> => {
|
|
1318
|
-
let lastError: Error
|
|
1319
|
-
|
|
1320
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1321
|
-
try {
|
|
1322
|
-
return await withTransaction(dbClient, transactionFn)
|
|
1323
|
-
} catch (error) {
|
|
1324
|
-
lastError = error as Error
|
|
1325
|
-
|
|
1326
|
-
// Check if error is retryable
|
|
1327
|
-
if (isRetryableError(error) && attempt < maxRetries) {
|
|
1328
|
-
const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
|
|
1329
|
-
console.warn(
|
|
1330
|
-
`Transaction attempt ${attempt} failed, retrying in ${delay}ms...`
|
|
1331
|
-
)
|
|
1332
|
-
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
1333
|
-
continue
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
throw error
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
throw lastError!
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
const isRetryableError = (error: any): boolean => {
|
|
1344
|
-
const retryableErrors = [
|
|
1345
|
-
'deadlock detected',
|
|
1346
|
-
'serialization failure',
|
|
1347
|
-
'connection lost',
|
|
1348
|
-
'timeout',
|
|
1349
|
-
]
|
|
1350
|
-
|
|
1351
|
-
return retryableErrors.some((msg) =>
|
|
1352
|
-
error.message?.toLowerCase().includes(msg)
|
|
1353
|
-
)
|
|
1354
|
-
}
|
|
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
|
|
1355
135
|
```
|
|
1356
136
|
|
|
1357
|
-
|
|
137
|
+
Tests live alongside each module in `__tests__/` directories under `src/core`, `src/db`, and `src/monitor`.
|
|
1358
138
|
|
|
1359
139
|
## Contributing
|
|
1360
140
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
### Development Setup
|
|
1364
|
-
|
|
1365
|
-
1. **Clone the repository**
|
|
1366
|
-
|
|
1367
|
-
```bash
|
|
1368
|
-
git clone https://github.com/starbem/star-db-query-builder.git
|
|
1369
|
-
cd star-db-query-builder
|
|
1370
|
-
```
|
|
1371
|
-
|
|
1372
|
-
2. **Install dependencies**
|
|
1373
|
-
|
|
1374
|
-
```bash
|
|
1375
|
-
pnpm install
|
|
1376
|
-
```
|
|
1377
|
-
|
|
1378
|
-
3. **Run tests**
|
|
1379
|
-
|
|
1380
|
-
```bash
|
|
1381
|
-
pnpm test
|
|
1382
|
-
```
|
|
1383
|
-
|
|
1384
|
-
4. **Run linting**
|
|
1385
|
-
|
|
1386
|
-
```bash
|
|
1387
|
-
pnpm lint
|
|
1388
|
-
```
|
|
1389
|
-
|
|
1390
|
-
5. **Build the project**
|
|
1391
|
-
```bash
|
|
1392
|
-
pnpm build
|
|
1393
|
-
```
|
|
1394
|
-
|
|
1395
|
-
### Contributing Guidelines
|
|
1396
|
-
|
|
1397
|
-
- **Code Style**: Follow the existing code style and use Prettier for formatting
|
|
1398
|
-
- **TypeScript**: Maintain strict TypeScript typing
|
|
1399
|
-
- **Tests**: Add tests for new features and bug fixes
|
|
1400
|
-
- **Documentation**: Update documentation for any API changes
|
|
1401
|
-
- **Commit Messages**: Use conventional commit messages
|
|
1402
|
-
|
|
1403
|
-
### Pull Request Process
|
|
1404
|
-
|
|
1405
|
-
1. Fork the repository
|
|
1406
|
-
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
1407
|
-
3. Make your changes
|
|
1408
|
-
4. Add tests for your changes
|
|
1409
|
-
5. Ensure all tests pass (`pnpm test`)
|
|
1410
|
-
6. Run linting (`pnpm lint`)
|
|
1411
|
-
7. Commit your changes (`git commit -m 'feat: add amazing feature'`)
|
|
1412
|
-
8. Push to your branch (`git push origin feature/amazing-feature`)
|
|
1413
|
-
9. Open a Pull Request
|
|
1414
|
-
|
|
1415
|
-
### Reporting Issues
|
|
1416
|
-
|
|
1417
|
-
When reporting issues, please include:
|
|
1418
|
-
|
|
1419
|
-
- **Environment**: Node.js version, database type and version
|
|
1420
|
-
- **Steps to Reproduce**: Clear steps to reproduce the issue
|
|
1421
|
-
- **Expected Behavior**: What you expected to happen
|
|
1422
|
-
- **Actual Behavior**: What actually happened
|
|
1423
|
-
- **Code Sample**: Minimal code sample that demonstrates the issue
|
|
1424
|
-
|
|
1425
|
-
### Feature Requests
|
|
1426
|
-
|
|
1427
|
-
For feature requests, please:
|
|
1428
|
-
|
|
1429
|
-
- **Describe the feature**: Clear description of what you want
|
|
1430
|
-
- **Use Case**: Explain why this feature would be useful
|
|
1431
|
-
- **Proposed API**: If you have ideas for the API design
|
|
1432
|
-
- **Alternatives**: Any alternative solutions you've considered
|
|
1433
|
-
|
|
1434
|
-
## License
|
|
1435
|
-
|
|
1436
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
1437
|
-
|
|
1438
|
-
## 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.
|
|
1439
142
|
|
|
1440
|
-
|
|
1441
|
-
- **Issues**: [GitHub Issues](https://github.com/starbem/star-db-query-builder/issues)
|
|
1442
|
-
- **Discussions**: [GitHub Discussions](https://github.com/starbem/star-db-query-builder/discussions)
|
|
143
|
+
## CI/CD
|
|
1443
144
|
|
|
1444
|
-
|
|
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.
|
|
1445
146
|
|
|
1446
|
-
|
|
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`).
|
|
1447
148
|
|
|
1448
|
-
|
|
149
|
+
## Release Process
|
|
1449
150
|
|
|
1450
|
-
|
|
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`).
|