reltype 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +954 -0
- package/README.md +227 -225
- package/dist/features/repository/base.d.ts.map +1 -1
- package/dist/features/repository/base.js +2 -1
- package/dist/features/schema/interfaces/Table.d.ts +18 -0
- package/dist/features/schema/interfaces/Table.d.ts.map +1 -1
- package/dist/features/schema/table.d.ts +35 -5
- package/dist/features/schema/table.d.ts.map +1 -1
- package/dist/features/schema/table.js +48 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,93 +8,95 @@
|
|
|
8
8
|
|
|
9
9
|
**Type-first relational modeling for PostgreSQL in TypeScript.**
|
|
10
10
|
|
|
11
|
-
PostgreSQL
|
|
11
|
+
Define your PostgreSQL tables in TypeScript code and get fully-typed query results automatically.
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
- **camelCase ↔ snake_case** — DB
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
13
|
+
- **Type-safe** — INSERT / SELECT / UPDATE types are automatically inferred from your schema
|
|
14
|
+
- **camelCase ↔ snake_case** — Automatic conversion between DB column names and TypeScript variables
|
|
15
|
+
- **Fluent query builder** — Chain `WHERE`, `OR`, `JOIN`, `GROUP BY`, `LIMIT`, `paginate`, `calculate`, `stream` and more
|
|
16
|
+
- **Large data optimization** — Cursor pagination, batch processing, AsyncGenerator streaming
|
|
17
|
+
- **Error classification** — `DbError` automatically classifies PostgreSQL errors into 13 distinct kinds
|
|
18
|
+
- **Hook system** — Before/after query lifecycle hooks for monitoring and APM integration
|
|
19
|
+
|
|
20
|
+
> 한국어 문서는 [README.ko.md](./README.ko.md) 를 참고하세요.
|
|
19
21
|
|
|
20
22
|
---
|
|
21
23
|
|
|
22
24
|
## Installation
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
# reltype
|
|
27
|
+
# Install reltype
|
|
26
28
|
npm install reltype
|
|
27
29
|
|
|
28
|
-
# pg
|
|
30
|
+
# pg is a peerDependency — install it separately
|
|
29
31
|
npm install pg
|
|
30
32
|
npm install --save-dev @types/pg
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
> `pg`
|
|
35
|
+
> Requires `pg` version 8.0.0 or higher.
|
|
34
36
|
|
|
35
37
|
---
|
|
36
38
|
|
|
37
39
|
## Environment Variables
|
|
38
40
|
|
|
39
|
-
`.env`
|
|
41
|
+
Create a `.env` file in your project root.
|
|
40
42
|
|
|
41
43
|
```env
|
|
42
|
-
# ──
|
|
44
|
+
# ── Required (either CONNECTION_STRING or DB_NAME must be set) ───────────────
|
|
43
45
|
|
|
44
|
-
#
|
|
46
|
+
# Option 1: Connection String (takes priority)
|
|
45
47
|
DB_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/mydb
|
|
46
48
|
|
|
47
|
-
#
|
|
49
|
+
# Option 2: Individual settings
|
|
48
50
|
DB_HOST=127.0.0.1
|
|
49
51
|
DB_PORT=5432
|
|
50
52
|
DB_NAME=mydb
|
|
51
53
|
DB_USER=postgres
|
|
52
54
|
DB_PASSWORD=postgres
|
|
53
55
|
|
|
54
|
-
# ──
|
|
56
|
+
# ── Optional ─────────────────────────────────────────────────────────────────
|
|
55
57
|
|
|
56
|
-
DB_SSL=false # SSL
|
|
57
|
-
DB_MAX=10 #
|
|
58
|
-
DB_IDLE_TIMEOUT=30000 #
|
|
59
|
-
DB_CONNECTION_TIMEOUT=2000 #
|
|
60
|
-
DB_ALLOW_EXIT_ON_IDLE=false #
|
|
61
|
-
DB_STATEMENT_TIMEOUT=0 # SQL
|
|
62
|
-
DB_QUERY_TIMEOUT=0 #
|
|
63
|
-
DB_APPLICATION_NAME=my-app #
|
|
64
|
-
DB_KEEP_ALIVE=true # TCP keep-alive
|
|
65
|
-
DB_KEEP_ALIVE_INITIAL_DELAY=10000 # keep-alive
|
|
58
|
+
DB_SSL=false # Enable SSL
|
|
59
|
+
DB_MAX=10 # Max connection pool size
|
|
60
|
+
DB_IDLE_TIMEOUT=30000 # Idle connection timeout (ms)
|
|
61
|
+
DB_CONNECTION_TIMEOUT=2000 # Connection acquisition timeout (ms)
|
|
62
|
+
DB_ALLOW_EXIT_ON_IDLE=false # Allow process exit when idle
|
|
63
|
+
DB_STATEMENT_TIMEOUT=0 # SQL statement timeout (ms, 0 = unlimited)
|
|
64
|
+
DB_QUERY_TIMEOUT=0 # Query timeout (ms, 0 = unlimited)
|
|
65
|
+
DB_APPLICATION_NAME=my-app # App name shown in pg_stat_activity
|
|
66
|
+
DB_KEEP_ALIVE=true # Enable TCP keep-alive
|
|
67
|
+
DB_KEEP_ALIVE_INITIAL_DELAY=10000 # Initial keep-alive delay (ms)
|
|
66
68
|
|
|
67
|
-
# ──
|
|
69
|
+
# ── Logging ───────────────────────────────────────────────────────────────────
|
|
68
70
|
|
|
69
|
-
LOGGER=true #
|
|
70
|
-
LOG_LEVEL=info #
|
|
71
|
+
LOGGER=true # Enable logger (true / false)
|
|
72
|
+
LOG_LEVEL=info # Log level (debug / info / log / warn / error)
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
---
|
|
74
76
|
|
|
75
77
|
## Quick Start
|
|
76
78
|
|
|
77
|
-
### 1.
|
|
79
|
+
### 1. Define a Table Schema
|
|
78
80
|
|
|
79
81
|
```ts
|
|
80
82
|
import { defineTable, col } from 'reltype';
|
|
81
83
|
|
|
82
84
|
export const usersTable = defineTable('users', {
|
|
83
|
-
id: col.serial().primaryKey(), // SERIAL PRIMARY KEY (
|
|
85
|
+
id: col.serial().primaryKey(), // SERIAL PRIMARY KEY (optional on INSERT)
|
|
84
86
|
firstName: col.varchar(255).notNull(), // VARCHAR(255) NOT NULL
|
|
85
|
-
lastName: col.varchar(255).nullable(), // VARCHAR(255) NULL (
|
|
87
|
+
lastName: col.varchar(255).nullable(), // VARCHAR(255) NULL (optional on INSERT)
|
|
86
88
|
email: col.text().notNull(), // TEXT NOT NULL
|
|
87
|
-
isActive: col.boolean().default(), // BOOLEAN DEFAULT ... (
|
|
88
|
-
createdAt: col.timestamptz().defaultNow(), // TIMESTAMPTZ DEFAULT NOW() (
|
|
89
|
+
isActive: col.boolean().default(), // BOOLEAN DEFAULT ... (optional on INSERT)
|
|
90
|
+
createdAt: col.timestamptz().defaultNow(), // TIMESTAMPTZ DEFAULT NOW() (optional on INSERT)
|
|
89
91
|
});
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
### 2.
|
|
94
|
+
### 2. Automatic Type Inference
|
|
93
95
|
|
|
94
96
|
```ts
|
|
95
97
|
import { InferRow, InferInsert, InferUpdate } from 'reltype';
|
|
96
98
|
|
|
97
|
-
// SELECT
|
|
99
|
+
// SELECT result type
|
|
98
100
|
type User = InferRow<typeof usersTable>;
|
|
99
101
|
// {
|
|
100
102
|
// id: number;
|
|
@@ -105,28 +107,28 @@ type User = InferRow<typeof usersTable>;
|
|
|
105
107
|
// createdAt: Date;
|
|
106
108
|
// }
|
|
107
109
|
|
|
108
|
-
// INSERT
|
|
110
|
+
// INSERT input type (optional columns automatically excluded)
|
|
109
111
|
type CreateUser = InferInsert<typeof usersTable>;
|
|
110
112
|
// { firstName: string; email: string; lastName?: string | null; isActive?: boolean; createdAt?: Date }
|
|
111
113
|
|
|
112
|
-
// UPDATE
|
|
114
|
+
// UPDATE input type (PK excluded, all fields optional)
|
|
113
115
|
type UpdateUser = InferUpdate<typeof usersTable>;
|
|
114
116
|
// { firstName?: string; lastName?: string | null; email?: string; isActive?: boolean; createdAt?: Date }
|
|
115
117
|
```
|
|
116
118
|
|
|
117
|
-
### 3.
|
|
119
|
+
### 3. Load dotenv at Application Entry Point
|
|
118
120
|
|
|
119
|
-
`reltype
|
|
121
|
+
`reltype` only reads `process.env`. Load your `.env` file **at the application entry point**.
|
|
120
122
|
|
|
121
123
|
```ts
|
|
122
|
-
//
|
|
123
|
-
import 'dotenv/config'; //
|
|
124
|
+
// Application entry point (index.ts / server.ts / app.ts)
|
|
125
|
+
import 'dotenv/config'; // Must be placed before other imports
|
|
124
126
|
|
|
125
|
-
//
|
|
127
|
+
// Then import reltype
|
|
126
128
|
import { getDatabaseConfig, getPool } from 'reltype';
|
|
127
129
|
```
|
|
128
130
|
|
|
129
|
-
### 4. Repository
|
|
131
|
+
### 4. Create a Repository
|
|
130
132
|
|
|
131
133
|
```ts
|
|
132
134
|
import { createRepo } from 'reltype';
|
|
@@ -139,34 +141,34 @@ export const userRepo = createRepo(usersTable);
|
|
|
139
141
|
|
|
140
142
|
## Repository API
|
|
141
143
|
|
|
142
|
-
###
|
|
144
|
+
### Method Summary
|
|
143
145
|
|
|
144
|
-
|
|
|
146
|
+
| Method | Return Type | Description |
|
|
145
147
|
|---|---|---|
|
|
146
|
-
| `create(data)` | `Promise<T>` |
|
|
147
|
-
| `update(id, data)` | `Promise<T \| null>` |
|
|
148
|
-
| `delete(id)` | `Promise<boolean>` |
|
|
148
|
+
| `create(data)` | `Promise<T>` | INSERT a single row |
|
|
149
|
+
| `update(id, data)` | `Promise<T \| null>` | UPDATE by primary key |
|
|
150
|
+
| `delete(id)` | `Promise<boolean>` | DELETE by primary key |
|
|
149
151
|
| `upsert(data, col?)` | `Promise<T>` | INSERT or UPDATE |
|
|
150
|
-
| `bulkCreate(rows)` | `Promise<T[]>` |
|
|
151
|
-
| `select(where?)` | `QueryBuilder<T>` |
|
|
152
|
-
| `selectOne(where)` | `Promise<T \| null>` |
|
|
153
|
-
| `raw(sql, params?)` | `Promise<R[]>` |
|
|
154
|
-
| `findAll(opts?)` | `Promise<T[]>` |
|
|
155
|
-
| `findById(id)` | `Promise<T \| null>` |
|
|
156
|
-
| `findOne(where)` | `Promise<T \| null>` |
|
|
157
|
-
| `useHooks(h)` | `this` |
|
|
152
|
+
| `bulkCreate(rows)` | `Promise<T[]>` | INSERT multiple rows |
|
|
153
|
+
| `select(where?)` | `QueryBuilder<T>` | Start a fluent query builder |
|
|
154
|
+
| `selectOne(where)` | `Promise<T \| null>` | Fetch a single row |
|
|
155
|
+
| `raw(sql, params?)` | `Promise<R[]>` | Execute raw SQL |
|
|
156
|
+
| `findAll(opts?)` | `Promise<T[]>` | Static full query |
|
|
157
|
+
| `findById(id)` | `Promise<T \| null>` | Fetch single row by PK |
|
|
158
|
+
| `findOne(where)` | `Promise<T \| null>` | Fetch single row by condition |
|
|
159
|
+
| `useHooks(h)` | `this` | Register global hooks |
|
|
158
160
|
|
|
159
161
|
---
|
|
160
162
|
|
|
161
163
|
## create
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
INSERT a single row. Columns with `serial`, `default`, or `nullable` modifiers are optional.
|
|
164
166
|
|
|
165
167
|
```ts
|
|
166
168
|
const user = await userRepo.create({
|
|
167
169
|
firstName: 'John',
|
|
168
170
|
email: 'john@example.com',
|
|
169
|
-
// lastName, isActive, createdAt → optional (DB default
|
|
171
|
+
// lastName, isActive, createdAt → optional (DB default or nullable)
|
|
170
172
|
});
|
|
171
173
|
// → User
|
|
172
174
|
```
|
|
@@ -175,10 +177,10 @@ const user = await userRepo.create({
|
|
|
175
177
|
|
|
176
178
|
## update
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
UPDATE only the specified columns by primary key. Returns `null` if the row does not exist.
|
|
179
181
|
|
|
180
182
|
```ts
|
|
181
|
-
//
|
|
183
|
+
// Partial update
|
|
182
184
|
const updated = await userRepo.update(1, {
|
|
183
185
|
firstName: 'Jane',
|
|
184
186
|
isActive: false,
|
|
@@ -186,7 +188,7 @@ const updated = await userRepo.update(1, {
|
|
|
186
188
|
// → User | null
|
|
187
189
|
|
|
188
190
|
if (!updated) {
|
|
189
|
-
throw new Error('
|
|
191
|
+
throw new Error('User not found.');
|
|
190
192
|
}
|
|
191
193
|
```
|
|
192
194
|
|
|
@@ -194,14 +196,14 @@ if (!updated) {
|
|
|
194
196
|
|
|
195
197
|
## delete
|
|
196
198
|
|
|
197
|
-
|
|
199
|
+
DELETE by primary key. Returns `true` if a row was deleted, `false` if not found.
|
|
198
200
|
|
|
199
201
|
```ts
|
|
200
202
|
const deleted = await userRepo.delete(1);
|
|
201
203
|
// → boolean
|
|
202
204
|
|
|
203
205
|
if (!deleted) {
|
|
204
|
-
throw new Error('
|
|
206
|
+
throw new Error('User not found.');
|
|
205
207
|
}
|
|
206
208
|
```
|
|
207
209
|
|
|
@@ -209,17 +211,17 @@ if (!deleted) {
|
|
|
209
211
|
|
|
210
212
|
## upsert
|
|
211
213
|
|
|
212
|
-
|
|
214
|
+
INSERT or UPDATE based on a conflict column.
|
|
213
215
|
|
|
214
216
|
```ts
|
|
215
|
-
// PK(id)
|
|
217
|
+
// By PK (id) — default
|
|
216
218
|
const user = await userRepo.upsert({
|
|
217
219
|
id: 1,
|
|
218
220
|
firstName: 'John',
|
|
219
221
|
email: 'john@example.com',
|
|
220
222
|
});
|
|
221
223
|
|
|
222
|
-
//
|
|
224
|
+
// By another unique column (snake_case)
|
|
223
225
|
const user = await userRepo.upsert(
|
|
224
226
|
{ firstName: 'John', email: 'john@example.com' },
|
|
225
227
|
'email',
|
|
@@ -231,7 +233,7 @@ const user = await userRepo.upsert(
|
|
|
231
233
|
|
|
232
234
|
## bulkCreate
|
|
233
235
|
|
|
234
|
-
|
|
236
|
+
Insert multiple rows with a single `INSERT` query.
|
|
235
237
|
|
|
236
238
|
```ts
|
|
237
239
|
const created = await userRepo.bulkCreate([
|
|
@@ -243,28 +245,28 @@ const created = await userRepo.bulkCreate([
|
|
|
243
245
|
|
|
244
246
|
---
|
|
245
247
|
|
|
246
|
-
## select —
|
|
248
|
+
## select — Fluent Query Builder
|
|
247
249
|
|
|
248
|
-
`repo.select(where?)
|
|
249
|
-
|
|
250
|
+
`repo.select(where?)` returns a `QueryBuilder`.
|
|
251
|
+
Chain methods and then `await` or call `.exec()` to execute.
|
|
250
252
|
|
|
251
|
-
###
|
|
253
|
+
### Basic Query
|
|
252
254
|
|
|
253
255
|
```ts
|
|
254
|
-
//
|
|
256
|
+
// Fetch all (await directly — thenable)
|
|
255
257
|
const users = await userRepo.select();
|
|
256
258
|
|
|
257
|
-
//
|
|
259
|
+
// With initial WHERE condition
|
|
258
260
|
const users = await userRepo.select({ isActive: true });
|
|
259
261
|
```
|
|
260
262
|
|
|
261
|
-
### WHERE — AND
|
|
263
|
+
### WHERE — AND Conditions
|
|
262
264
|
|
|
263
265
|
```ts
|
|
264
|
-
//
|
|
266
|
+
// Simple equality
|
|
265
267
|
const users = await userRepo.select().where({ isActive: true });
|
|
266
268
|
|
|
267
|
-
//
|
|
269
|
+
// Comparison operator
|
|
268
270
|
const users = await userRepo.select()
|
|
269
271
|
.where({ createdAt: { operator: '>=', value: new Date('2024-01-01') } });
|
|
270
272
|
|
|
@@ -276,17 +278,17 @@ const users = await userRepo.select()
|
|
|
276
278
|
const users = await userRepo.select()
|
|
277
279
|
.where({ deletedAt: { operator: 'IS NULL' } });
|
|
278
280
|
|
|
279
|
-
// LIKE / ILIKE (
|
|
281
|
+
// LIKE / ILIKE (case-insensitive)
|
|
280
282
|
const users = await userRepo.select()
|
|
281
283
|
.where({ email: { operator: 'ILIKE', value: '%@gmail.com' } });
|
|
282
284
|
```
|
|
283
285
|
|
|
284
|
-
|
|
286
|
+
Supported operators: `=` `!=` `>` `<` `>=` `<=` `LIKE` `ILIKE` `IN` `NOT IN` `IS NULL` `IS NOT NULL`
|
|
285
287
|
|
|
286
|
-
### OR — OR
|
|
288
|
+
### OR — OR Conditions
|
|
287
289
|
|
|
288
|
-
`.or()
|
|
289
|
-
AND
|
|
290
|
+
Each `.or()` call adds an OR clause.
|
|
291
|
+
When AND conditions are present, the result is `WHERE (AND conditions) OR (OR conditions)`.
|
|
290
292
|
|
|
291
293
|
```ts
|
|
292
294
|
// firstName ILIKE '%john%' OR email ILIKE '%john%'
|
|
@@ -302,7 +304,7 @@ const users = await userRepo.select({ isActive: true })
|
|
|
302
304
|
const users = await userRepo.select()
|
|
303
305
|
.orderBy([{ column: 'createdAt', direction: 'DESC' }]);
|
|
304
306
|
|
|
305
|
-
//
|
|
307
|
+
// Multiple sort columns
|
|
306
308
|
const users = await userRepo.select()
|
|
307
309
|
.orderBy([
|
|
308
310
|
{ column: 'isActive', direction: 'DESC' },
|
|
@@ -317,7 +319,7 @@ const users = await userRepo.select()
|
|
|
317
319
|
.orderBy([{ column: 'id', direction: 'ASC' }])
|
|
318
320
|
.limit(20)
|
|
319
321
|
.offset(40);
|
|
320
|
-
// 3
|
|
322
|
+
// Page 3 (0-indexed offset)
|
|
321
323
|
```
|
|
322
324
|
|
|
323
325
|
### GROUP BY
|
|
@@ -343,9 +345,9 @@ const result = await userRepo.select({ isActive: true })
|
|
|
343
345
|
.exec();
|
|
344
346
|
```
|
|
345
347
|
|
|
346
|
-
JOIN
|
|
348
|
+
JOIN types: `INNER` `LEFT` `RIGHT` `FULL`
|
|
347
349
|
|
|
348
|
-
###
|
|
350
|
+
### Column Selection (columns)
|
|
349
351
|
|
|
350
352
|
```ts
|
|
351
353
|
const users = await userRepo.select()
|
|
@@ -357,7 +359,7 @@ const users = await userRepo.select()
|
|
|
357
359
|
|
|
358
360
|
## selectOne
|
|
359
361
|
|
|
360
|
-
`select(where).one()
|
|
362
|
+
Shorthand for `select(where).one()`. Returns the first matching row.
|
|
361
363
|
|
|
362
364
|
```ts
|
|
363
365
|
const user = await userRepo.selectOne({ email: 'john@example.com' });
|
|
@@ -369,16 +371,16 @@ if (!user) throw new Error('not found');
|
|
|
369
371
|
|
|
370
372
|
---
|
|
371
373
|
|
|
372
|
-
## calculate —
|
|
374
|
+
## calculate — Aggregate Functions
|
|
373
375
|
|
|
374
|
-
`COUNT`, `SUM`, `AVG`, `MIN`, `MAX
|
|
376
|
+
Runs `COUNT`, `SUM`, `AVG`, `MIN`, `MAX` aggregations.
|
|
375
377
|
|
|
376
378
|
```ts
|
|
377
|
-
//
|
|
379
|
+
// Total count
|
|
378
380
|
const result = await userRepo.select().calculate([{ fn: 'COUNT', alias: 'count' }]);
|
|
379
381
|
const total = parseInt(String(result.count), 10);
|
|
380
382
|
|
|
381
|
-
//
|
|
383
|
+
// Multiple aggregations
|
|
382
384
|
const stats = await userRepo.select({ isActive: true })
|
|
383
385
|
.calculate([
|
|
384
386
|
{ fn: 'COUNT', alias: 'count' },
|
|
@@ -390,89 +392,89 @@ const stats = await userRepo.select({ isActive: true })
|
|
|
390
392
|
|
|
391
393
|
---
|
|
392
394
|
|
|
393
|
-
## paginate — OFFSET
|
|
395
|
+
## paginate — OFFSET Pagination
|
|
394
396
|
|
|
395
|
-
COUNT
|
|
397
|
+
Runs COUNT and DATA queries in parallel.
|
|
396
398
|
|
|
397
399
|
```ts
|
|
398
400
|
const result = await userRepo.select({ isActive: true })
|
|
399
401
|
.orderBy([{ column: 'createdAt', direction: 'DESC' }])
|
|
400
402
|
.paginate({ page: 1, pageSize: 20 });
|
|
401
403
|
|
|
402
|
-
// result
|
|
404
|
+
// result shape
|
|
403
405
|
// {
|
|
404
|
-
// data: User[], //
|
|
405
|
-
// count: 150, //
|
|
406
|
+
// data: User[], // Current page data
|
|
407
|
+
// count: 150, // Total matching rows
|
|
406
408
|
// page: 1,
|
|
407
409
|
// pageSize: 20,
|
|
408
|
-
// nextAction: true, //
|
|
409
|
-
// previousAction: false, //
|
|
410
|
+
// nextAction: true, // Next page exists
|
|
411
|
+
// previousAction: false, // Previous page exists
|
|
410
412
|
// }
|
|
411
413
|
```
|
|
412
414
|
|
|
413
|
-
>
|
|
415
|
+
> For tables with millions of rows, use `cursorPaginate()` instead.
|
|
414
416
|
|
|
415
417
|
---
|
|
416
418
|
|
|
417
|
-
## cursorPaginate —
|
|
419
|
+
## cursorPaginate — Cursor-based Pagination (Large Data)
|
|
418
420
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
+
Uses `WHERE id > last_id` instead of OFFSET scanning.
|
|
422
|
+
Assigning an indexed column as `cursorColumn` ensures consistent speed even with tens of millions of rows.
|
|
421
423
|
|
|
422
424
|
```ts
|
|
423
|
-
//
|
|
425
|
+
// First page
|
|
424
426
|
const p1 = await userRepo.select({ isActive: true })
|
|
425
427
|
.cursorPaginate({ pageSize: 20, cursorColumn: 'id' });
|
|
426
428
|
|
|
427
429
|
// p1 = { data: [...], nextCursor: 'xxx', pageSize: 20, hasNext: true }
|
|
428
430
|
|
|
429
|
-
//
|
|
431
|
+
// Next page
|
|
430
432
|
if (p1.hasNext) {
|
|
431
433
|
const p2 = await userRepo.select({ isActive: true })
|
|
432
434
|
.cursorPaginate({ pageSize: 20, cursorColumn: 'id', cursor: p1.nextCursor });
|
|
433
435
|
}
|
|
434
436
|
|
|
435
|
-
//
|
|
437
|
+
// Descending cursor (createdAt DESC)
|
|
436
438
|
const result = await userRepo.select()
|
|
437
439
|
.cursorPaginate({ pageSize: 20, cursorColumn: 'createdAt', direction: 'desc' });
|
|
438
440
|
```
|
|
439
441
|
|
|
440
442
|
| `paginate` | `cursorPaginate` |
|
|
441
443
|
|---|---|
|
|
442
|
-
|
|
|
443
|
-
| page
|
|
444
|
-
|
|
|
444
|
+
| Provides total count | No total count |
|
|
445
|
+
| Navigate by page number | Next / previous only |
|
|
446
|
+
| Slows down on large tables | Consistent speed always |
|
|
445
447
|
|
|
446
448
|
---
|
|
447
449
|
|
|
448
|
-
## forEach —
|
|
450
|
+
## forEach — Batch Processing
|
|
449
451
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
+
Processes data in chunks without loading everything into memory.
|
|
453
|
+
Ideal for large-scale ETL, bulk email sending, and data migration.
|
|
452
454
|
|
|
453
455
|
```ts
|
|
454
456
|
await userRepo.select({ isActive: true })
|
|
455
457
|
.orderBy([{ column: 'id', direction: 'ASC' }])
|
|
456
458
|
.forEach(async (batch) => {
|
|
457
|
-
// batch: User[] (
|
|
459
|
+
// batch: User[] (default 500 rows per chunk)
|
|
458
460
|
await sendEmailBatch(batch);
|
|
459
461
|
}, { batchSize: 200 });
|
|
460
462
|
```
|
|
461
463
|
|
|
462
464
|
---
|
|
463
465
|
|
|
464
|
-
## stream —
|
|
466
|
+
## stream — Streaming (AsyncGenerator)
|
|
465
467
|
|
|
466
|
-
`for await...of
|
|
467
|
-
|
|
468
|
+
Iterates rows one by one with `for await...of`.
|
|
469
|
+
Internally fetches in batches to keep memory usage low.
|
|
468
470
|
|
|
469
471
|
```ts
|
|
470
|
-
// for await...of
|
|
472
|
+
// Direct for await...of (Symbol.asyncIterator supported)
|
|
471
473
|
for await (const user of userRepo.select({ isActive: true })) {
|
|
472
474
|
await processRow(user);
|
|
473
475
|
}
|
|
474
476
|
|
|
475
|
-
//
|
|
477
|
+
// With custom batch size
|
|
476
478
|
for await (const user of userRepo.select().stream({ batchSize: 1000 })) {
|
|
477
479
|
await processRow(user);
|
|
478
480
|
}
|
|
@@ -480,19 +482,19 @@ for await (const user of userRepo.select().stream({ batchSize: 1000 })) {
|
|
|
480
482
|
|
|
481
483
|
---
|
|
482
484
|
|
|
483
|
-
## raw — Raw SQL
|
|
485
|
+
## raw — Raw SQL Execution
|
|
484
486
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
+
Write SQL directly when complex queries are needed.
|
|
488
|
+
Result column names are automatically converted from `snake_case` to `camelCase`.
|
|
487
489
|
|
|
488
490
|
```ts
|
|
489
|
-
// repo.raw()
|
|
491
|
+
// repo.raw()
|
|
490
492
|
const users = await userRepo.raw<UserRow>(
|
|
491
493
|
'SELECT * FROM users WHERE first_name ILIKE $1 ORDER BY created_at DESC',
|
|
492
494
|
['%john%'],
|
|
493
495
|
);
|
|
494
496
|
|
|
495
|
-
// QueryBuilder.raw() —
|
|
497
|
+
// QueryBuilder.raw() — standalone, no repository needed
|
|
496
498
|
import { QueryBuilder } from 'reltype';
|
|
497
499
|
|
|
498
500
|
const rows = await QueryBuilder.raw(
|
|
@@ -507,16 +509,16 @@ const rows = await QueryBuilder.raw(
|
|
|
507
509
|
|
|
508
510
|
---
|
|
509
511
|
|
|
510
|
-
## explain —
|
|
512
|
+
## explain — Query Plan Analysis
|
|
511
513
|
|
|
512
|
-
|
|
514
|
+
Inspect index usage and identify performance bottlenecks.
|
|
513
515
|
|
|
514
516
|
```ts
|
|
515
517
|
// EXPLAIN
|
|
516
518
|
const plan = await userRepo.select({ isActive: true }).explain();
|
|
517
519
|
console.log(plan);
|
|
518
520
|
|
|
519
|
-
// EXPLAIN ANALYZE (
|
|
521
|
+
// EXPLAIN ANALYZE (includes actual execution statistics)
|
|
520
522
|
const plan = await userRepo.select({ isActive: true })
|
|
521
523
|
.orderBy([{ column: 'createdAt', direction: 'DESC' }])
|
|
522
524
|
.explain(true);
|
|
@@ -524,9 +526,9 @@ const plan = await userRepo.select({ isActive: true })
|
|
|
524
526
|
|
|
525
527
|
---
|
|
526
528
|
|
|
527
|
-
## toSQL — SQL
|
|
529
|
+
## toSQL — Preview SQL (Debugging)
|
|
528
530
|
|
|
529
|
-
|
|
531
|
+
Returns the generated SQL and params without executing the query.
|
|
530
532
|
|
|
531
533
|
```ts
|
|
532
534
|
const { sql, params } = userRepo.select({ isActive: true })
|
|
@@ -542,46 +544,46 @@ console.log(params);
|
|
|
542
544
|
|
|
543
545
|
---
|
|
544
546
|
|
|
545
|
-
## hooks —
|
|
547
|
+
## hooks — Query Lifecycle Hooks
|
|
546
548
|
|
|
547
|
-
###
|
|
549
|
+
### Per-query Hooks
|
|
548
550
|
|
|
549
551
|
```ts
|
|
550
552
|
const users = await userRepo.select({ isActive: true })
|
|
551
553
|
.hooks({
|
|
552
|
-
beforeExec: ({ sql, params }) => logger.debug('
|
|
554
|
+
beforeExec: ({ sql, params }) => logger.debug('About to run SQL:', sql),
|
|
553
555
|
afterExec: ({ rows, elapsed }) => metrics.record('db.query.duration', elapsed),
|
|
554
556
|
onError: ({ err, sql }) => alerting.send({ err, sql }),
|
|
555
557
|
})
|
|
556
558
|
.paginate({ page: 1, pageSize: 20 });
|
|
557
559
|
```
|
|
558
560
|
|
|
559
|
-
###
|
|
561
|
+
### Repository-level Global Hooks
|
|
560
562
|
|
|
561
|
-
|
|
563
|
+
Automatically applied to all `select()` builders on this repository.
|
|
562
564
|
|
|
563
565
|
```ts
|
|
564
566
|
userRepo.useHooks({
|
|
565
567
|
beforeExec: ({ sql }) => logger.debug('SQL:', sql),
|
|
566
568
|
afterExec: ({ elapsed }) => metrics.histogram('db.latency', elapsed),
|
|
567
|
-
onError: ({ err }) => logger.error('DB
|
|
569
|
+
onError: ({ err }) => logger.error('DB error', { message: err.message }),
|
|
568
570
|
});
|
|
569
571
|
|
|
570
|
-
//
|
|
572
|
+
// All subsequent select() calls will use the hooks
|
|
571
573
|
const users = await userRepo.select({ isActive: true }).exec();
|
|
572
574
|
```
|
|
573
575
|
|
|
574
576
|
---
|
|
575
577
|
|
|
576
|
-
##
|
|
578
|
+
## Static CRUD (findAll / findById / findOne)
|
|
577
579
|
|
|
578
|
-
|
|
580
|
+
Use static methods for simple queries.
|
|
579
581
|
|
|
580
582
|
```ts
|
|
581
|
-
//
|
|
583
|
+
// Fetch all
|
|
582
584
|
const users = await userRepo.findAll();
|
|
583
585
|
|
|
584
|
-
//
|
|
586
|
+
// With filter, sort, and pagination
|
|
585
587
|
const users = await userRepo.findAll({
|
|
586
588
|
where: { isActive: true },
|
|
587
589
|
orderBy: [{ col: 'createdAt', dir: 'DESC' }],
|
|
@@ -589,20 +591,20 @@ const users = await userRepo.findAll({
|
|
|
589
591
|
offset: 0,
|
|
590
592
|
});
|
|
591
593
|
|
|
592
|
-
//
|
|
594
|
+
// Single row by PK
|
|
593
595
|
const user = await userRepo.findById(1); // User | null
|
|
594
596
|
|
|
595
|
-
//
|
|
597
|
+
// Single row by condition (equality only)
|
|
596
598
|
const user = await userRepo.findOne({ email: 'john@example.com' }); // User | null
|
|
597
599
|
```
|
|
598
600
|
|
|
599
|
-
>
|
|
601
|
+
> For operators like `LIKE`, `IN`, or `OR`, use `repo.select()` instead.
|
|
600
602
|
|
|
601
603
|
---
|
|
602
604
|
|
|
603
605
|
## Column Types
|
|
604
606
|
|
|
605
|
-
|
|
|
607
|
+
| Method | PostgreSQL Type | TypeScript Type |
|
|
606
608
|
|---|---|---|
|
|
607
609
|
| `col.serial()` | `SERIAL` | `number` |
|
|
608
610
|
| `col.integer()` | `INTEGER` | `number` |
|
|
@@ -615,16 +617,16 @@ const user = await userRepo.findOne({ email: 'john@example.com' }); // User | nu
|
|
|
615
617
|
| `col.timestamptz()` | `TIMESTAMPTZ` | `Date` |
|
|
616
618
|
| `col.date()` | `DATE` | `Date` |
|
|
617
619
|
| `col.uuid()` | `UUID` | `string` |
|
|
618
|
-
| `col.jsonb<T>()` | `JSONB` | `T` (
|
|
620
|
+
| `col.jsonb<T>()` | `JSONB` | `T` (default `unknown`) |
|
|
619
621
|
|
|
620
622
|
### Column Modifiers
|
|
621
623
|
|
|
622
624
|
```ts
|
|
623
|
-
col.text().notNull()
|
|
624
|
-
col.text().nullable()
|
|
625
|
-
col.integer().primaryKey()
|
|
626
|
-
col.boolean().default()
|
|
627
|
-
col.timestamptz().defaultNow() // DEFAULT NOW(),
|
|
625
|
+
col.text().notNull() // NOT NULL (default state)
|
|
626
|
+
col.text().nullable() // Allow NULL, optional on INSERT
|
|
627
|
+
col.integer().primaryKey() // PRIMARY KEY, optional on INSERT
|
|
628
|
+
col.boolean().default() // DB DEFAULT, optional on INSERT
|
|
629
|
+
col.timestamptz().defaultNow() // DEFAULT NOW(), optional on INSERT
|
|
628
630
|
```
|
|
629
631
|
|
|
630
632
|
---
|
|
@@ -639,7 +641,7 @@ const result = await runInTx(async (client) => {
|
|
|
639
641
|
await userRepo.create({ firstName: 'Bob', email: 'bob@example.com' });
|
|
640
642
|
return 'done';
|
|
641
643
|
});
|
|
642
|
-
//
|
|
644
|
+
// Automatically rolls back if any operation fails
|
|
643
645
|
```
|
|
644
646
|
|
|
645
647
|
---
|
|
@@ -649,16 +651,16 @@ const result = await runInTx(async (client) => {
|
|
|
649
651
|
```ts
|
|
650
652
|
import { getPool, withClient, closePool } from 'reltype';
|
|
651
653
|
|
|
652
|
-
//
|
|
654
|
+
// Direct pool access
|
|
653
655
|
const pool = getPool();
|
|
654
656
|
|
|
655
|
-
//
|
|
657
|
+
// Borrow a client and run a raw query
|
|
656
658
|
const rows = await withClient(async (client) => {
|
|
657
659
|
const result = await client.query('SELECT NOW()');
|
|
658
660
|
return result.rows;
|
|
659
661
|
});
|
|
660
662
|
|
|
661
|
-
//
|
|
663
|
+
// On application shutdown
|
|
662
664
|
await closePool();
|
|
663
665
|
```
|
|
664
666
|
|
|
@@ -666,7 +668,7 @@ await closePool();
|
|
|
666
668
|
|
|
667
669
|
## Raw Query Builders
|
|
668
670
|
|
|
669
|
-
|
|
671
|
+
Build queries directly without a repository.
|
|
670
672
|
|
|
671
673
|
```ts
|
|
672
674
|
import { buildSelect, buildInsert, buildUpdate, buildDelete, buildUpsert, buildBulkInsert, withClient } from 'reltype';
|
|
@@ -696,14 +698,14 @@ const built = buildBulkInsert('users', [
|
|
|
696
698
|
{ firstName: 'Bob', email: 'bob@example.com' },
|
|
697
699
|
]);
|
|
698
700
|
|
|
699
|
-
//
|
|
701
|
+
// Execute
|
|
700
702
|
await withClient(async (client) => {
|
|
701
703
|
const result = await client.query(sql, params);
|
|
702
704
|
return result.rows;
|
|
703
705
|
});
|
|
704
706
|
```
|
|
705
707
|
|
|
706
|
-
>
|
|
708
|
+
> All query builders automatically convert camelCase keys to snake_case column names.
|
|
707
709
|
|
|
708
710
|
---
|
|
709
711
|
|
|
@@ -740,13 +742,13 @@ logger.warn('warn message');
|
|
|
740
742
|
logger.error('error message', new Error('oops'));
|
|
741
743
|
```
|
|
742
744
|
|
|
743
|
-
|
|
745
|
+
Enable with environment variables: `LOGGER=true`, `LOG_LEVEL=debug`.
|
|
744
746
|
|
|
745
747
|
---
|
|
746
748
|
|
|
747
749
|
## Extending BaseRepo
|
|
748
750
|
|
|
749
|
-
|
|
751
|
+
Extend `BaseRepo` to add custom methods.
|
|
750
752
|
|
|
751
753
|
```ts
|
|
752
754
|
import { BaseRepo, InferRow } from 'reltype';
|
|
@@ -769,10 +771,10 @@ export const userRepo = new UserRepo(usersTable);
|
|
|
769
771
|
|
|
770
772
|
## Error Handling
|
|
771
773
|
|
|
772
|
-
### DbError — PostgreSQL
|
|
774
|
+
### DbError — PostgreSQL Error Classification
|
|
773
775
|
|
|
774
|
-
|
|
775
|
-
`DbError
|
|
776
|
+
All DB errors are automatically converted to `DbError`.
|
|
777
|
+
`DbError` separates internal log details from user-facing messages.
|
|
776
778
|
|
|
777
779
|
```ts
|
|
778
780
|
import { DbError } from 'reltype';
|
|
@@ -781,23 +783,23 @@ try {
|
|
|
781
783
|
await userRepo.create({ firstName: 'Alice', email: 'alice@example.com' });
|
|
782
784
|
} catch (err) {
|
|
783
785
|
if (err instanceof DbError) {
|
|
784
|
-
//
|
|
786
|
+
// Safe to expose to users
|
|
785
787
|
console.log(err.toUserPayload());
|
|
786
|
-
// { error: '
|
|
788
|
+
// { error: 'A duplicate value already exists.', kind: 'uniqueViolation', isRetryable: false }
|
|
787
789
|
|
|
788
|
-
//
|
|
790
|
+
// Internal logging details
|
|
789
791
|
console.log(err.toLogContext());
|
|
790
792
|
// { pgCode: '23505', kind: 'uniqueViolation', table: 'users', constraint: '...', ... }
|
|
791
793
|
|
|
792
|
-
//
|
|
794
|
+
// Check if retryable
|
|
793
795
|
if (err.isRetryable) {
|
|
794
|
-
//
|
|
796
|
+
// retry logic
|
|
795
797
|
}
|
|
796
798
|
}
|
|
797
799
|
}
|
|
798
800
|
```
|
|
799
801
|
|
|
800
|
-
###
|
|
802
|
+
### Example with Express
|
|
801
803
|
|
|
802
804
|
```ts
|
|
803
805
|
app.post('/users', async (req, res) => {
|
|
@@ -812,29 +814,29 @@ app.post('/users', async (req, res) => {
|
|
|
812
814
|
: 500;
|
|
813
815
|
res.status(status).json(err.toUserPayload());
|
|
814
816
|
} else {
|
|
815
|
-
res.status(500).json({ error: '
|
|
817
|
+
res.status(500).json({ error: 'An unexpected error occurred.' });
|
|
816
818
|
}
|
|
817
819
|
}
|
|
818
820
|
});
|
|
819
821
|
```
|
|
820
822
|
|
|
821
|
-
### DbErrorKind
|
|
823
|
+
### DbErrorKind Reference
|
|
822
824
|
|
|
823
|
-
| kind | PostgreSQL SQLSTATE |
|
|
825
|
+
| kind | PostgreSQL SQLSTATE | Description | isRetryable |
|
|
824
826
|
|---|---|---|---|
|
|
825
|
-
| `uniqueViolation` | 23505 | UNIQUE
|
|
826
|
-
| `foreignKeyViolation` | 23503 |
|
|
827
|
-
| `notNullViolation` | 23502 | NOT NULL
|
|
828
|
-
| `checkViolation` | 23514 | CHECK
|
|
829
|
-
| `deadlock` | 40P01 |
|
|
830
|
-
| `serializationFailure` | 40001 |
|
|
831
|
-
| `connectionFailed` | 08xxx |
|
|
832
|
-
| `tooManyConnections` | 53300 |
|
|
833
|
-
| `queryTimeout` | 57014 |
|
|
834
|
-
| `undefinedTable` | 42P01 |
|
|
835
|
-
| `undefinedColumn` | 42703 |
|
|
836
|
-
| `invalidInput` | 22xxx |
|
|
837
|
-
| `unknown` |
|
|
827
|
+
| `uniqueViolation` | 23505 | UNIQUE constraint violation | false |
|
|
828
|
+
| `foreignKeyViolation` | 23503 | Foreign key constraint violation | false |
|
|
829
|
+
| `notNullViolation` | 23502 | NOT NULL constraint violation | false |
|
|
830
|
+
| `checkViolation` | 23514 | CHECK constraint violation | false |
|
|
831
|
+
| `deadlock` | 40P01 | Deadlock detected | **true** |
|
|
832
|
+
| `serializationFailure` | 40001 | Serialization failure | **true** |
|
|
833
|
+
| `connectionFailed` | 08xxx | Connection failure | **true** |
|
|
834
|
+
| `tooManyConnections` | 53300 | Too many connections | **true** |
|
|
835
|
+
| `queryTimeout` | 57014 | Query timeout | false |
|
|
836
|
+
| `undefinedTable` | 42P01 | Table does not exist | false |
|
|
837
|
+
| `undefinedColumn` | 42703 | Column does not exist | false |
|
|
838
|
+
| `invalidInput` | 22xxx | Invalid input format | false |
|
|
839
|
+
| `unknown` | other | Unclassified error | false |
|
|
838
840
|
|
|
839
841
|
---
|
|
840
842
|
|
|
@@ -843,75 +845,75 @@ app.post('/users', async (req, res) => {
|
|
|
843
845
|
```ts
|
|
844
846
|
import { getPoolStatus, checkPoolHealth } from 'reltype';
|
|
845
847
|
|
|
846
|
-
//
|
|
848
|
+
// Get current pool status
|
|
847
849
|
const status = getPoolStatus();
|
|
848
850
|
console.log(status);
|
|
849
851
|
// {
|
|
850
|
-
// totalCount: 5, //
|
|
851
|
-
// idleCount: 3, //
|
|
852
|
-
// waitingCount: 0, //
|
|
853
|
-
// isHealthy: true //
|
|
852
|
+
// totalCount: 5, // Total connections created
|
|
853
|
+
// idleCount: 3, // Idle connections
|
|
854
|
+
// waitingCount: 0, // Requests waiting for a connection
|
|
855
|
+
// isHealthy: true // Pool is healthy
|
|
854
856
|
// }
|
|
855
857
|
|
|
856
|
-
//
|
|
858
|
+
// Health check against the DB server (SELECT 1)
|
|
857
859
|
const isAlive = await checkPoolHealth();
|
|
858
860
|
```
|
|
859
861
|
|
|
860
|
-
### Too Many Connections
|
|
862
|
+
### Preventing Too Many Connections
|
|
861
863
|
|
|
862
|
-
|
|
864
|
+
Always configure pool size and timeouts in your `.env`.
|
|
863
865
|
|
|
864
866
|
```env
|
|
865
|
-
DB_MAX=10 #
|
|
866
|
-
DB_CONNECTION_TIMEOUT=3000 #
|
|
867
|
-
DB_IDLE_TIMEOUT=30000 #
|
|
868
|
-
DB_STATEMENT_TIMEOUT=10000 # SQL
|
|
867
|
+
DB_MAX=10 # Max pool size (default: 10)
|
|
868
|
+
DB_CONNECTION_TIMEOUT=3000 # Connection acquisition timeout in ms (infinite wait if not set — warning)
|
|
869
|
+
DB_IDLE_TIMEOUT=30000 # Idle connection release time in ms
|
|
870
|
+
DB_STATEMENT_TIMEOUT=10000 # Max SQL statement execution time in ms
|
|
869
871
|
```
|
|
870
872
|
|
|
871
|
-
> `DB_CONNECTION_TIMEOUT
|
|
872
|
-
>
|
|
873
|
+
> If `DB_CONNECTION_TIMEOUT` is not set, requests will wait indefinitely when the pool is exhausted.
|
|
874
|
+
> Always configure this value.
|
|
873
875
|
|
|
874
876
|
---
|
|
875
877
|
|
|
876
878
|
## Log System
|
|
877
879
|
|
|
878
|
-
###
|
|
880
|
+
### Format Configuration
|
|
879
881
|
|
|
880
882
|
```env
|
|
881
|
-
LOGGER=true #
|
|
883
|
+
LOGGER=true # Enable logger
|
|
882
884
|
LOG_LEVEL=debug # debug / info / log / warn / error
|
|
883
|
-
LOG_FORMAT=json # text(
|
|
885
|
+
LOG_FORMAT=json # text (default) / json (recommended for production)
|
|
884
886
|
```
|
|
885
887
|
|
|
886
|
-
### text
|
|
888
|
+
### text format (development)
|
|
887
889
|
|
|
888
890
|
```
|
|
889
|
-
2024-01-01T00:00:00.000Z [Pool] INFO
|
|
891
|
+
2024-01-01T00:00:00.000Z [Pool] INFO Pool created { max: 10, ... }
|
|
890
892
|
2024-01-01T00:00:00.000Z [Repo] DEBUG SQL: SELECT * FROM users WHERE id = $1 [ 1 ]
|
|
891
|
-
2024-01-01T00:00:00.000Z [Repo] DEBUG
|
|
893
|
+
2024-01-01T00:00:00.000Z [Repo] DEBUG Done (12ms) rowCount=1
|
|
892
894
|
```
|
|
893
895
|
|
|
894
|
-
### json
|
|
896
|
+
### json format (production / log aggregators)
|
|
895
897
|
|
|
896
898
|
```json
|
|
897
|
-
{"ts":"2024-01-01T00:00:00.000Z","level":"INFO","prefix":"[Pool]","msg":"Pool
|
|
898
|
-
{"ts":"2024-01-01T00:00:00.000Z","level":"ERROR","prefix":"[Repo]","msg":"
|
|
899
|
+
{"ts":"2024-01-01T00:00:00.000Z","level":"INFO","prefix":"[Pool]","msg":"Pool created","meta":[{"max":10}]}
|
|
900
|
+
{"ts":"2024-01-01T00:00:00.000Z","level":"ERROR","prefix":"[Repo]","msg":"Query failed [users]","meta":[{"pgCode":"23505","kind":"uniqueViolation","constraint":"users_email_key"}]}
|
|
899
901
|
```
|
|
900
902
|
|
|
901
|
-
###
|
|
903
|
+
### Log Event Reference
|
|
902
904
|
|
|
903
|
-
|
|
|
905
|
+
| Level | Prefix | Event |
|
|
904
906
|
|---|---|---|
|
|
905
|
-
| INFO | [Pool] | Pool
|
|
906
|
-
| WARN | [Pool] | connectionTimeoutMillis
|
|
907
|
-
| WARN | [Pool] |
|
|
908
|
-
| DEBUG | [Pool] |
|
|
909
|
-
| ERROR | [Pool] |
|
|
910
|
-
| DEBUG | [Repo] | SQL
|
|
911
|
-
| ERROR | [Repo] |
|
|
912
|
-
| DEBUG | [Tx] |
|
|
913
|
-
| WARN | [Tx] |
|
|
914
|
-
| ERROR | [Tx] |
|
|
907
|
+
| INFO | [Pool] | Pool created / Pool closed |
|
|
908
|
+
| WARN | [Pool] | connectionTimeoutMillis not configured |
|
|
909
|
+
| WARN | [Pool] | Max connections reached |
|
|
910
|
+
| DEBUG | [Pool] | New connection / Connection removed |
|
|
911
|
+
| ERROR | [Pool] | Idle client error / Client acquisition failed |
|
|
912
|
+
| DEBUG | [Repo] | SQL executed + elapsed time |
|
|
913
|
+
| ERROR | [Repo] | Query failed (pgCode, kind, elapsed included) |
|
|
914
|
+
| DEBUG | [Tx] | Transaction started / committed |
|
|
915
|
+
| WARN | [Tx] | Transaction rolled back |
|
|
916
|
+
| ERROR | [Tx] | Rollback failed |
|
|
915
917
|
|
|
916
918
|
---
|
|
917
919
|
|
|
@@ -919,16 +921,16 @@ LOG_FORMAT=json # text(기본) / json(프로덕션 권장)
|
|
|
919
921
|
|
|
920
922
|
```
|
|
921
923
|
src/
|
|
922
|
-
├── index.ts ←
|
|
923
|
-
├── configs/env.ts ← DB
|
|
924
|
+
├── index.ts ← Public API entry point
|
|
925
|
+
├── configs/env.ts ← DB config parsing
|
|
924
926
|
├── utils/
|
|
925
|
-
│ ├── logger.ts ← Logger
|
|
926
|
-
│ └── reader.ts ←
|
|
927
|
+
│ ├── logger.ts ← Logger class
|
|
928
|
+
│ └── reader.ts ← Env parser, PostgresConfig
|
|
927
929
|
└── features/
|
|
928
930
|
├── schema/ ← defineTable, col, InferRow/Insert/Update
|
|
929
|
-
├── transform/ ← camelCase ↔ snake_case
|
|
930
|
-
├── connection/ ← Pool
|
|
931
|
-
├── query/ ← SQL
|
|
931
|
+
├── transform/ ← camelCase ↔ snake_case conversion
|
|
932
|
+
├── connection/ ← Pool management, Transaction
|
|
933
|
+
├── query/ ← SQL query builders (select/insert/update/delete/upsert/bulkInsert)
|
|
932
934
|
└── repository/ ← BaseRepo, createRepo, IRepo
|
|
933
935
|
```
|
|
934
936
|
|
|
@@ -936,14 +938,14 @@ src/
|
|
|
936
938
|
|
|
937
939
|
## Contributing
|
|
938
940
|
|
|
939
|
-
|
|
941
|
+
Bug reports, feature suggestions, and pull requests are all welcome.
|
|
940
942
|
→ [Issues](https://github.com/psh-suhyun/reltype/issues) · [Pull Requests](https://github.com/psh-suhyun/reltype/pulls)
|
|
941
943
|
|
|
942
944
|
---
|
|
943
945
|
|
|
944
946
|
## Changelog
|
|
945
947
|
|
|
946
|
-
|
|
948
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the full version history.
|
|
947
949
|
|
|
948
950
|
---
|
|
949
951
|
|