@dcl/pg-component 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/dist/src/component.d.ts +18 -0
- package/dist/src/component.d.ts.map +1 -0
- package/dist/src/component.js +248 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/metrics.d.ts +7 -0
- package/dist/src/metrics.d.ts.map +1 -0
- package/dist/src/metrics.js +15 -0
- package/dist/src/types.d.ts +84 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @dcl/pg-component
|
|
2
|
+
|
|
3
|
+
A PostgreSQL database component that provides connection pooling, transaction management, query streaming, and migration support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @dcl/pg-component
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createPgComponent } from '@dcl/pg-component'
|
|
15
|
+
import SQL from 'sql-template-strings'
|
|
16
|
+
|
|
17
|
+
// Create the component with required dependencies
|
|
18
|
+
const pg = await createPgComponent({ config, logs, metrics })
|
|
19
|
+
|
|
20
|
+
// Start the component (runs migrations if configured)
|
|
21
|
+
await pg.start()
|
|
22
|
+
|
|
23
|
+
// Execute queries using sql-template-strings for safe parameterization
|
|
24
|
+
const result = await pg.query<{ id: number; name: string }>(SQL`SELECT * FROM users WHERE id = ${userId}`)
|
|
25
|
+
|
|
26
|
+
// Stop the component (gracefully drains connections)
|
|
27
|
+
await pg.stop()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Connection pooling**: Efficient connection management using `pg` Pool
|
|
33
|
+
- **SQL injection protection**: Use `sql-template-strings` for safe parameterized queries
|
|
34
|
+
- **Transaction support**: Two transaction APIs for different use cases
|
|
35
|
+
- **Query streaming**: Memory-efficient streaming for large result sets
|
|
36
|
+
- **Migration support**: Built-in support for `node-pg-migrate`
|
|
37
|
+
- **Metrics integration**: Optional query duration metrics
|
|
38
|
+
- **Graceful shutdown**: Drains connections before closing the pool
|
|
39
|
+
|
|
40
|
+
## Transactions
|
|
41
|
+
|
|
42
|
+
### Using `withTransaction`
|
|
43
|
+
|
|
44
|
+
Provides direct access to the transaction client:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
await pg.withTransaction(async (client) => {
|
|
48
|
+
await client.query('INSERT INTO users (name) VALUES ($1)', ['Alice'])
|
|
49
|
+
await client.query('INSERT INTO audit (action) VALUES ($1)', ['user_created'])
|
|
50
|
+
// Automatically commits on success, rolls back on error
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Using `withAsyncContextTransaction`
|
|
55
|
+
|
|
56
|
+
Uses AsyncLocalStorage so nested `query()` calls automatically use the transaction client:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
await pg.withAsyncContextTransaction(async () => {
|
|
60
|
+
// All pg.query() calls within this callback use the same transaction
|
|
61
|
+
await pg.query(SQL`INSERT INTO users (name) VALUES ('Alice')`)
|
|
62
|
+
await pg.query(SQL`INSERT INTO audit (action) VALUES ('user_created')`)
|
|
63
|
+
// Automatically commits on success, rolls back on error
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Important Warnings
|
|
68
|
+
|
|
69
|
+
#### Do not use transaction control statements with `withAsyncContextTransaction`
|
|
70
|
+
|
|
71
|
+
When using `withAsyncContextTransaction`, do **not** execute `BEGIN`, `COMMIT`, or `ROLLBACK` via `query()`. The transaction lifecycle is managed automatically:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// ❌ WRONG - Don't do this
|
|
75
|
+
await pg.withAsyncContextTransaction(async () => {
|
|
76
|
+
await pg.query(SQL`BEGIN`) // Don't do this!
|
|
77
|
+
await pg.query(SQL`INSERT INTO users (name) VALUES ('Alice')`)
|
|
78
|
+
await pg.query(SQL`COMMIT`) // Don't do this!
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// ✅ CORRECT
|
|
82
|
+
await pg.withAsyncContextTransaction(async () => {
|
|
83
|
+
await pg.query(SQL`INSERT INTO users (name) VALUES ('Alice')`)
|
|
84
|
+
// BEGIN/COMMIT/ROLLBACK are handled automatically
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Nesting transactions creates independent transactions
|
|
89
|
+
|
|
90
|
+
Calling `withTransaction` or `withAsyncContextTransaction` inside another transaction method will create **independent transactions**, not nested transactions. Each call acquires a new connection from the pool:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// ⚠️ WARNING: This creates TWO independent transactions
|
|
94
|
+
await pg.withAsyncContextTransaction(async () => {
|
|
95
|
+
await pg.query(SQL`INSERT INTO table1 (name) VALUES ('outer')`)
|
|
96
|
+
|
|
97
|
+
// This is a SEPARATE transaction with its own connection!
|
|
98
|
+
await pg.withTransaction(async (client) => {
|
|
99
|
+
await client.query(`INSERT INTO table2 (name) VALUES ('inner')`)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If the inner transaction fails and rolls back, the outer transaction is **not** affected and will still commit. This is because PostgreSQL does not support true nested transactions, and each transaction method acquires its own connection.
|
|
105
|
+
|
|
106
|
+
## Query Streaming
|
|
107
|
+
|
|
108
|
+
For large result sets, use `streamQuery` to avoid loading all rows into memory:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
for await (const row of pg.streamQuery<User>(SQL`SELECT * FROM large_table`)) {
|
|
112
|
+
await processRow(row)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Migrations
|
|
117
|
+
|
|
118
|
+
Configure migrations when creating the component:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const pg = await createPgComponent(
|
|
122
|
+
{ config, logs },
|
|
123
|
+
{
|
|
124
|
+
migration: {
|
|
125
|
+
migrationsTable: 'pgmigrations',
|
|
126
|
+
dir: path.join(__dirname, 'migrations'),
|
|
127
|
+
direction: 'up',
|
|
128
|
+
count: Infinity
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Environment variables read by the component:
|
|
137
|
+
|
|
138
|
+
| Variable | Type | Description |
|
|
139
|
+
| ------------------------------------- | -------- | ---------------------------------------- |
|
|
140
|
+
| `PG_COMPONENT_PSQL_CONNECTION_STRING` | `string` | PostgreSQL connection string |
|
|
141
|
+
| `PG_COMPONENT_PSQL_HOST` | `string` | Database host |
|
|
142
|
+
| `PG_COMPONENT_PSQL_PORT` | `number` | Database port |
|
|
143
|
+
| `PG_COMPONENT_PSQL_DATABASE` | `string` | Database name |
|
|
144
|
+
| `PG_COMPONENT_PSQL_USER` | `string` | Database user |
|
|
145
|
+
| `PG_COMPONENT_PSQL_PASSWORD` | `string` | Database password |
|
|
146
|
+
| `PG_COMPONENT_IDLE_TIMEOUT` | `number` | Idle connection timeout (ms) |
|
|
147
|
+
| `PG_COMPONENT_QUERY_TIMEOUT` | `number` | Query timeout (ms) |
|
|
148
|
+
| `PG_COMPONENT_STREAM_QUERY_TIMEOUT` | `number` | Stream query timeout (ms) |
|
|
149
|
+
| `PG_COMPONENT_GRACE_PERIODS` | `number` | Grace periods for shutdown (default: 10) |
|
|
150
|
+
|
|
151
|
+
## Metrics
|
|
152
|
+
|
|
153
|
+
When a metrics component is provided, query durations are tracked:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Pass a label to track query duration
|
|
157
|
+
const result = await pg.query(SQL`SELECT * FROM users`, 'get_users')
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Metric: `dcl_db_query_duration_seconds` with labels `query` and `status` (success/error)
|
|
161
|
+
|
|
162
|
+
## Testing
|
|
163
|
+
|
|
164
|
+
Tests use [Testcontainers](https://testcontainers.com/) to run against a real PostgreSQL instance:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Requires Docker to be running
|
|
168
|
+
pnpm test
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
Apache-2.0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { IBaseComponent, IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces';
|
|
2
|
+
import { Options, IPgComponent, IMetricsComponent } from './types';
|
|
3
|
+
export * from './types';
|
|
4
|
+
export * from './metrics';
|
|
5
|
+
export declare function runReportingQueryDurationMetric<T>(components: {
|
|
6
|
+
metrics: IMetricsComponent;
|
|
7
|
+
}, queryNameLabel: string, functionToRun: () => Promise<T>): Promise<T>;
|
|
8
|
+
/**
|
|
9
|
+
* Query a Postgres (https://www.postgresql.org) database with ease.
|
|
10
|
+
* It uses a pool behind the scenes and will try to gracefully close it after finishing the connection.
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export declare function createPgComponent(components: {
|
|
14
|
+
logs: ILoggerComponent;
|
|
15
|
+
config: IConfigComponent;
|
|
16
|
+
metrics?: IMetricsComponent;
|
|
17
|
+
}, options?: Options): Promise<IPgComponent & IBaseComponent>;
|
|
18
|
+
//# sourceMappingURL=component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAOtG,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAwC,MAAM,SAAS,CAAA;AAExG,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AAEzB,wBAAsB,+BAA+B,CAAC,CAAC,EACrD,UAAU,EAAE;IAAE,OAAO,EAAE,iBAAiB,CAAA;CAAE,EAC1C,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC9B,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAE,EAC7F,OAAO,GAAE,OAAY,GACpB,OAAO,CAAC,YAAY,GAAG,cAAc,CAAC,CA0OxC"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.runReportingQueryDurationMetric = runReportingQueryDurationMetric;
|
|
21
|
+
exports.createPgComponent = createPgComponent;
|
|
22
|
+
const async_hooks_1 = require("async_hooks");
|
|
23
|
+
const pg_1 = require("pg");
|
|
24
|
+
const pg_query_stream_1 = __importDefault(require("pg-query-stream"));
|
|
25
|
+
const node_pg_migrate_1 = __importDefault(require("node-pg-migrate"));
|
|
26
|
+
const promises_1 = require("timers/promises");
|
|
27
|
+
__exportStar(require("./types"), exports);
|
|
28
|
+
__exportStar(require("./metrics"), exports);
|
|
29
|
+
async function runReportingQueryDurationMetric(components, queryNameLabel, functionToRun) {
|
|
30
|
+
const { metrics } = components;
|
|
31
|
+
const { end: endTimer } = metrics.startTimer('dcl_db_query_duration_seconds', {
|
|
32
|
+
query: queryNameLabel
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
const res = await functionToRun();
|
|
36
|
+
endTimer({ status: 'success' });
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
endTimer({ status: 'error' });
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Query a Postgres (https://www.postgresql.org) database with ease.
|
|
46
|
+
* It uses a pool behind the scenes and will try to gracefully close it after finishing the connection.
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
async function createPgComponent(components, options = {}) {
|
|
50
|
+
const { config, logs } = components;
|
|
51
|
+
const logger = logs.getLogger('pg-component');
|
|
52
|
+
// Environment
|
|
53
|
+
const [connectionString, port, host, database, user, password, idleTimeoutMillis, query_timeout] = await Promise.all([
|
|
54
|
+
config.getString('PG_COMPONENT_PSQL_CONNECTION_STRING'),
|
|
55
|
+
config.getNumber('PG_COMPONENT_PSQL_PORT'),
|
|
56
|
+
config.getString('PG_COMPONENT_PSQL_HOST'),
|
|
57
|
+
config.getString('PG_COMPONENT_PSQL_DATABASE'),
|
|
58
|
+
config.getString('PG_COMPONENT_PSQL_USER'),
|
|
59
|
+
config.getString('PG_COMPONENT_PSQL_PASSWORD'),
|
|
60
|
+
config.getNumber('PG_COMPONENT_IDLE_TIMEOUT'),
|
|
61
|
+
config.getNumber('PG_COMPONENT_QUERY_TIMEOUT')
|
|
62
|
+
]);
|
|
63
|
+
const defaultOptions = {
|
|
64
|
+
connectionString,
|
|
65
|
+
port,
|
|
66
|
+
host,
|
|
67
|
+
database,
|
|
68
|
+
user,
|
|
69
|
+
password,
|
|
70
|
+
idleTimeoutMillis,
|
|
71
|
+
query_timeout
|
|
72
|
+
};
|
|
73
|
+
const STREAM_QUERY_TIMEOUT = await config.getNumber('PG_COMPONENT_STREAM_QUERY_TIMEOUT');
|
|
74
|
+
const GRACE_PERIODS = (await config.getNumber('PG_COMPONENT_GRACE_PERIODS')) || 10;
|
|
75
|
+
const finalOptions = { ...defaultOptions, ...options.pool };
|
|
76
|
+
if (!finalOptions.log) {
|
|
77
|
+
finalOptions.log = logger.debug.bind(logger);
|
|
78
|
+
}
|
|
79
|
+
// Config
|
|
80
|
+
const pool = new pg_1.Pool(finalOptions);
|
|
81
|
+
// Async context for transaction client
|
|
82
|
+
const transactionContext = new async_hooks_1.AsyncLocalStorage();
|
|
83
|
+
// Methods
|
|
84
|
+
async function start() {
|
|
85
|
+
try {
|
|
86
|
+
const db = await pool.connect();
|
|
87
|
+
try {
|
|
88
|
+
if (options.migration) {
|
|
89
|
+
logger.debug('Running migrations:');
|
|
90
|
+
const opt = {
|
|
91
|
+
...options.migration,
|
|
92
|
+
dbClient: db
|
|
93
|
+
};
|
|
94
|
+
if (!opt.logger) {
|
|
95
|
+
opt.logger = logger;
|
|
96
|
+
}
|
|
97
|
+
await (0, node_pg_migrate_1.default)(opt);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
logger.error(err);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
db.release();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
logger.warn('Error starting pg-component:');
|
|
110
|
+
logger.error(error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function withTransaction(callback) {
|
|
115
|
+
const client = await pool.connect();
|
|
116
|
+
try {
|
|
117
|
+
await client.query('BEGIN');
|
|
118
|
+
const result = await callback(client);
|
|
119
|
+
await client.query('COMMIT');
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
await client.query('ROLLBACK');
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
client.release();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function withAsyncContextTransaction(callback) {
|
|
131
|
+
const client = await pool.connect();
|
|
132
|
+
try {
|
|
133
|
+
await client.query('BEGIN');
|
|
134
|
+
const result = await transactionContext.run(client, callback);
|
|
135
|
+
await client.query('COMMIT');
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
await client.query('ROLLBACK');
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
client.release();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function defaultQuery(sql) {
|
|
147
|
+
const notices = [];
|
|
148
|
+
// Get the transaction's context client or connect a new one
|
|
149
|
+
const transactionClient = transactionContext.getStore();
|
|
150
|
+
const client = transactionClient ?? (await pool.connect());
|
|
151
|
+
function listenNotice(notice) {
|
|
152
|
+
notices.push(notice);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
client.on('notice', listenNotice);
|
|
156
|
+
const result = await client.query(sql);
|
|
157
|
+
return { ...result, rowCount: result.rowCount ?? 0, notices };
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
client.off('notice', listenNotice);
|
|
161
|
+
// Only release if we created a new connection (not from transaction context)
|
|
162
|
+
if (!transactionClient) {
|
|
163
|
+
client.release();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function measuredQuery(sql, durationQueryNameLabel) {
|
|
168
|
+
const result = durationQueryNameLabel
|
|
169
|
+
? await runReportingQueryDurationMetric({ metrics: components.metrics }, durationQueryNameLabel, () => defaultQuery(sql))
|
|
170
|
+
: await defaultQuery(sql);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
async function* streamQuery(sql, config) {
|
|
174
|
+
const client = new pg_1.Client({
|
|
175
|
+
...finalOptions,
|
|
176
|
+
query_timeout: STREAM_QUERY_TIMEOUT
|
|
177
|
+
});
|
|
178
|
+
await client.connect();
|
|
179
|
+
// https://github.com/brianc/node-postgres/issues/1860
|
|
180
|
+
// Uncaught TypeError: queryCallback is not a function
|
|
181
|
+
// finish - OK, this call is necessary to finish the query when we configure query_timeout due to a bug in pg
|
|
182
|
+
// finish - with error, this call is necessary to finish the query when we configure query_timeout due to a bug in pg
|
|
183
|
+
const stream = new pg_query_stream_1.default(sql.text, sql.values, config);
|
|
184
|
+
stream.callback = function () {
|
|
185
|
+
// noop
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
client.query(stream);
|
|
189
|
+
for await (const row of stream) {
|
|
190
|
+
yield row;
|
|
191
|
+
}
|
|
192
|
+
stream.callback(undefined, undefined);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
stream.callback(err, undefined);
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
stream.destroy();
|
|
200
|
+
await client.end();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let didStop = false;
|
|
204
|
+
async function stop() {
|
|
205
|
+
if (didStop) {
|
|
206
|
+
logger.error('Stop called more than once');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
didStop = true;
|
|
210
|
+
let gracePeriods = GRACE_PERIODS;
|
|
211
|
+
while (gracePeriods > 0 && pool.waitingCount > 0) {
|
|
212
|
+
logger.debug('Draining connections', {
|
|
213
|
+
waitingCount: pool.waitingCount,
|
|
214
|
+
gracePeriods
|
|
215
|
+
});
|
|
216
|
+
await (0, promises_1.setTimeout)(200);
|
|
217
|
+
gracePeriods -= 1;
|
|
218
|
+
}
|
|
219
|
+
const promise = pool.end();
|
|
220
|
+
let finished = false;
|
|
221
|
+
promise.finally(() => {
|
|
222
|
+
finished = true;
|
|
223
|
+
});
|
|
224
|
+
while (!finished && (pool.totalCount > 0 || pool.idleCount > 0 || pool.waitingCount > 0)) {
|
|
225
|
+
if (pool.totalCount) {
|
|
226
|
+
logger.log('Draining connections', {
|
|
227
|
+
totalCount: pool.totalCount,
|
|
228
|
+
idleCount: pool.idleCount,
|
|
229
|
+
waitingCount: pool.waitingCount
|
|
230
|
+
});
|
|
231
|
+
await (0, promises_1.setTimeout)(1000);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
await promise;
|
|
235
|
+
}
|
|
236
|
+
function getPool() {
|
|
237
|
+
return pool;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
query: components.metrics ? measuredQuery : defaultQuery,
|
|
241
|
+
withTransaction,
|
|
242
|
+
withAsyncContextTransaction,
|
|
243
|
+
streamQuery,
|
|
244
|
+
getPool,
|
|
245
|
+
start,
|
|
246
|
+
stop
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,SAAS,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./component"), exports);
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { IMetricsComponent } from "@well-known-components/interfaces";
|
|
2
|
+
/**
|
|
3
|
+
* Metrics declarations, needed for your IMetricsComponent
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export declare const metricDeclarations: IMetricsComponent.MetricsRecordDefinition<string>;
|
|
7
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAA;AAErE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,iBAAiB,CAAC,uBAAuB,CAAC,MAAM,CAMhF,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.metricDeclarations = void 0;
|
|
4
|
+
const interfaces_1 = require("@well-known-components/interfaces");
|
|
5
|
+
/**
|
|
6
|
+
* Metrics declarations, needed for your IMetricsComponent
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
exports.metricDeclarations = {
|
|
10
|
+
dcl_db_query_duration_seconds: {
|
|
11
|
+
help: "Histogram of query duration to the database in seconds per query",
|
|
12
|
+
type: interfaces_1.IMetricsComponent.HistogramType,
|
|
13
|
+
labelNames: ["query", "status"], // status=(success|error)
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { IDatabase, IMetricsComponent as IBaseMetricsComponent } from '@well-known-components/interfaces';
|
|
2
|
+
import { Pool, PoolClient, PoolConfig } from 'pg';
|
|
3
|
+
import { NoticeMessage } from 'pg-protocol/dist/messages';
|
|
4
|
+
import { RunnerOption } from 'node-pg-migrate';
|
|
5
|
+
import { SQLStatement } from 'sql-template-strings';
|
|
6
|
+
import QueryStream from 'pg-query-stream';
|
|
7
|
+
import { metricDeclarations } from './metrics';
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export type QueryStreamWithCallback = QueryStream & {
|
|
12
|
+
callback: Function;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* @public
|
|
16
|
+
*
|
|
17
|
+
* Query result with notices.
|
|
18
|
+
*/
|
|
19
|
+
export type QueryResult<T extends Record<string, any>> = IDatabase.IQueryResult<T> & {
|
|
20
|
+
notices: NoticeMessage[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
export type Options = Partial<{
|
|
26
|
+
pool: PoolConfig;
|
|
27
|
+
migration: Omit<RunnerOption, 'databaseUrl' | 'dbClient'>;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export interface IPgComponent extends IDatabase {
|
|
33
|
+
start(): Promise<void>;
|
|
34
|
+
query<T extends Record<string, any>>(sql: string): Promise<QueryResult<T>>;
|
|
35
|
+
query<T extends Record<string, any>>(sql: SQLStatement, durationQueryNameLabel?: string): Promise<QueryResult<T>>;
|
|
36
|
+
streamQuery<T = any>(sql: SQLStatement, config?: {
|
|
37
|
+
batchSize?: number;
|
|
38
|
+
}): AsyncGenerator<T>;
|
|
39
|
+
/**
|
|
40
|
+
* Executes a callback within a transaction using a client.
|
|
41
|
+
* The client is acquired from the pool and released after the callback is executed.
|
|
42
|
+
* If an error occurs, the transaction is rolled back and the client is released.
|
|
43
|
+
*
|
|
44
|
+
* @warning Nesting transaction methods (calling `withTransaction` or `withAsyncContextTransaction`
|
|
45
|
+
* inside this callback) will create independent transactions, not nested transactions.
|
|
46
|
+
* Each call acquires a new connection from the pool.
|
|
47
|
+
*/
|
|
48
|
+
withTransaction<T>(callback: (client: PoolClient) => Promise<T>): Promise<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Executes a callback within a transaction using async context.
|
|
51
|
+
* The client is acquired from the pool and released after the callback is executed.
|
|
52
|
+
* If an error occurs, the transaction is rolled back and the client is released.
|
|
53
|
+
* All calls to query() within the callback will automatically use the transaction's client.
|
|
54
|
+
*
|
|
55
|
+
* @warning Do not execute transaction control statements (BEGIN, COMMIT, ROLLBACK) via `query()`
|
|
56
|
+
* within this callback, as the transaction lifecycle is managed automatically.
|
|
57
|
+
*
|
|
58
|
+
* @warning Nesting transaction methods (calling `withTransaction` or `withAsyncContextTransaction`
|
|
59
|
+
* inside this callback) will create independent transactions, not nested transactions.
|
|
60
|
+
* Each call acquires a new connection from the pool.
|
|
61
|
+
*/
|
|
62
|
+
withAsyncContextTransaction<T>(callback: () => Promise<T>): Promise<T>;
|
|
63
|
+
/**
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
getPool(): Pool;
|
|
67
|
+
stop(): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
export declare namespace IPgComponent {
|
|
73
|
+
/**
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
type Composable = {
|
|
77
|
+
pg: IPgComponent;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
export type IMetricsComponent = IBaseMetricsComponent<keyof typeof metricDeclarations>;
|
|
84
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,iBAAiB,IAAI,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AACzG,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,WAAW,MAAM,iBAAiB,CAAA;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAE9C;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAA;AAE1E;;;;GAIG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG;IACnF,OAAO,EAAE,aAAa,EAAE,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,UAAU,CAAC,CAAA;CAAE,CAAC,CAAA;AAE9G;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEtB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1E,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,sBAAsB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IACjH,WAAW,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;IAC3F;;;;;;;;OAQG;IACH,eAAe,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC5E;;;;;;;;;;;;OAYG;IACH,2BAA2B,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAEtE;;OAEG;IACH,OAAO,IAAI,IAAI,CAAA;IAEf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACtB;AAED;;GAEG;AACH,yBAAiB,YAAY,CAAC;IAC5B;;OAEG;IACH,KAAY,UAAU,GAAG;QACvB,EAAE,EAAE,YAAY,CAAA;KACjB,CAAA;CACF;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dcl/pg-component",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PG component for core components library",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"types": "dist/src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@well-known-components/interfaces": "^1.5.2",
|
|
12
|
+
"node-pg-migrate": "^7.9.1",
|
|
13
|
+
"pg": "8.17.2",
|
|
14
|
+
"pg-protocol": "^1.11.0",
|
|
15
|
+
"pg-query-stream": "4.11.2",
|
|
16
|
+
"sql-template-strings": "2.2.2"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@well-known-components/interfaces": "^1.5.2"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@testcontainers/postgresql": "^10.18.0",
|
|
23
|
+
"@types/pg": "^8.16.0",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"dev": "tsc --watch",
|
|
32
|
+
"clean": "rm -rf dist",
|
|
33
|
+
"test": "jest --forceExit",
|
|
34
|
+
"lint": "echo \"No linting configured\""
|
|
35
|
+
}
|
|
36
|
+
}
|