@grest-ts/db-postgre 0.0.5
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/LICENSE +21 -0
- package/dist/src/GGPostgres.d.ts +30 -0
- package/dist/src/GGPostgres.d.ts.map +1 -0
- package/dist/src/GGPostgres.js +118 -0
- package/dist/src/GGPostgres.js.map +1 -0
- package/dist/src/GGPostgresConfig.d.ts +26 -0
- package/dist/src/GGPostgresConfig.d.ts.map +1 -0
- package/dist/src/GGPostgresConfig.js +32 -0
- package/dist/src/GGPostgresConfig.js.map +1 -0
- package/dist/src/GGPostgresConnection.d.ts +82 -0
- package/dist/src/GGPostgresConnection.d.ts.map +1 -0
- package/dist/src/GGPostgresConnection.js +135 -0
- package/dist/src/GGPostgresConnection.js.map +1 -0
- package/dist/src/index-node.d.ts +5 -0
- package/dist/src/index-node.d.ts.map +1 -0
- package/dist/src/index-node.js +4 -0
- package/dist/src/index-node.js.map +1 -0
- package/dist/src/tsconfig.json +16 -0
- package/dist/testkit/GGPostgresSchemaCloner.d.ts +13 -0
- package/dist/testkit/GGPostgresSchemaCloner.d.ts.map +1 -0
- package/dist/testkit/GGPostgresSchemaCloner.js +51 -0
- package/dist/testkit/GGPostgresSchemaCloner.js.map +1 -0
- package/dist/testkit/GGPostgresSchemaOperations.d.ts +20 -0
- package/dist/testkit/GGPostgresSchemaOperations.d.ts.map +1 -0
- package/dist/testkit/GGPostgresSchemaOperations.js +111 -0
- package/dist/testkit/GGPostgresSchemaOperations.js.map +1 -0
- package/dist/testkit/GGPostgresTestMethods.d.ts +44 -0
- package/dist/testkit/GGPostgresTestMethods.d.ts.map +1 -0
- package/dist/testkit/GGPostgresTestMethods.js +103 -0
- package/dist/testkit/GGPostgresTestMethods.js.map +1 -0
- package/dist/testkit/index-testkit.d.ts +3 -0
- package/dist/testkit/index-testkit.d.ts.map +1 -0
- package/dist/testkit/index-testkit.js +3 -0
- package/dist/testkit/index-testkit.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/package.json +62 -0
- package/src/GGPostgres.ts +138 -0
- package/src/GGPostgresConfig.ts +39 -0
- package/src/GGPostgresConnection.ts +146 -0
- package/src/index-node.ts +4 -0
- package/src/tsconfig.json +16 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {Pool, QueryResult, QueryResultRow} from 'pg';
|
|
2
|
+
import {GGLocator, GGLocatorServiceType} from '@grest-ts/locator';
|
|
3
|
+
import {GGLog} from '@grest-ts/logger';
|
|
4
|
+
import {GGPostgresConnection} from './GGPostgresConnection';
|
|
5
|
+
import type {GGPostgresConfig} from "./GGPostgresConfig";
|
|
6
|
+
|
|
7
|
+
export class GGPostgres {
|
|
8
|
+
|
|
9
|
+
private readonly config: GGPostgresConfig;
|
|
10
|
+
|
|
11
|
+
private started = false;
|
|
12
|
+
|
|
13
|
+
private pool: Pool | undefined = undefined;
|
|
14
|
+
private unwatchHost: (() => void) | undefined = undefined;
|
|
15
|
+
private unwatchUser: (() => void) | undefined = undefined;
|
|
16
|
+
|
|
17
|
+
constructor(config: GGPostgresConfig) {
|
|
18
|
+
this.config = config
|
|
19
|
+
|
|
20
|
+
this.unwatchHost = this.config.host.watch(() => this.connect());
|
|
21
|
+
this.unwatchUser = this.config.user.watch(() => this.connect());
|
|
22
|
+
|
|
23
|
+
GGLocator.getScope().setWithLifecycle(this.config.token, this, {
|
|
24
|
+
type: GGLocatorServiceType.DATABASE,
|
|
25
|
+
start: () => this.start(),
|
|
26
|
+
teardown: () => this.teardown(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async connect(): Promise<void> {
|
|
31
|
+
if (!this.started) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const config = this.config.host.get();
|
|
36
|
+
const user = this.config.user.reveal();
|
|
37
|
+
const connectionsLimit = config.connectionLimit ?? 20;
|
|
38
|
+
|
|
39
|
+
const newPool = new Pool({
|
|
40
|
+
host: config.host ?? "localhost",
|
|
41
|
+
port: config.port ?? 5432,
|
|
42
|
+
user: user.username,
|
|
43
|
+
password: user.password,
|
|
44
|
+
database: config.database,
|
|
45
|
+
max: connectionsLimit,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const client = await newPool.connect();
|
|
50
|
+
await client.query('SELECT 1');
|
|
51
|
+
client.release();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
GGLog.critical(this, 'Failed to connect to pool! Must resolve immediately, new services will fail to start!', {
|
|
54
|
+
database: config.database,
|
|
55
|
+
host: config.host,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err)
|
|
57
|
+
});
|
|
58
|
+
await newPool.end();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.pool) {
|
|
63
|
+
this.pool.end().catch(err => {
|
|
64
|
+
GGLog.warn(this, 'Config change: error closing old pool', {
|
|
65
|
+
error: err instanceof Error ? err.message : String(err)
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.pool = newPool;
|
|
71
|
+
|
|
72
|
+
GGLog.info(this, 'Postgres connected!', {database: config.database});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async start(): Promise<void> {
|
|
76
|
+
this.started = true;
|
|
77
|
+
await this.connect();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async teardown(): Promise<void> {
|
|
81
|
+
this.unwatchHost?.();
|
|
82
|
+
this.unwatchHost = undefined;
|
|
83
|
+
this.unwatchUser?.();
|
|
84
|
+
this.unwatchUser = undefined;
|
|
85
|
+
if (this.pool) {
|
|
86
|
+
await this.pool.end();
|
|
87
|
+
this.pool = undefined;
|
|
88
|
+
GGLog.debug(this, 'disconnected');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private getPool(): Pool {
|
|
93
|
+
if (!this.pool) {
|
|
94
|
+
throw new Error(`Postgres '${this.config.name}' not connected. Are you calling this before runtime.start()?`);
|
|
95
|
+
}
|
|
96
|
+
return this.pool;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async query<T extends QueryResultRow = QueryResultRow>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
100
|
+
GGLog.debug(this, 'query', {sql, params});
|
|
101
|
+
const result: QueryResult<T> = await this.getPool().query<T>(sql, params);
|
|
102
|
+
GGLog.debug(this, 'query result', {rowCount: result.rowCount});
|
|
103
|
+
return result.rows;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public async execute(sql: string, params?: unknown[]): Promise<QueryResult> {
|
|
107
|
+
GGLog.debug(this, 'execute', {sql, params});
|
|
108
|
+
const result = await this.getPool().query(sql, params);
|
|
109
|
+
GGLog.debug(this, 'execute result', {rowCount: result.rowCount});
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ==================== Connection for transactions ====================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get a dedicated connection from the pool.
|
|
117
|
+
* Use this for transactions or when you need multiple queries on the same connection.
|
|
118
|
+
*
|
|
119
|
+
* IMPORTANT: Always call release() on the connection when done.
|
|
120
|
+
*/
|
|
121
|
+
public async getConnection(): Promise<GGPostgresConnection> {
|
|
122
|
+
const client = await this.getPool().connect();
|
|
123
|
+
return new GGPostgresConnection(client);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run a callback within a transaction.
|
|
128
|
+
* Automatically handles connection lifecycle, commits on success, rolls back on failure.
|
|
129
|
+
*/
|
|
130
|
+
public async runInTransaction<T>(callback: (conn: GGPostgresConnection) => Promise<T>): Promise<T> {
|
|
131
|
+
const conn = await this.getConnection();
|
|
132
|
+
try {
|
|
133
|
+
return await conn.runInTransaction(() => callback(conn));
|
|
134
|
+
} finally {
|
|
135
|
+
conn.release();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {GGResource, GGSecret} from "@grest-ts/config";
|
|
2
|
+
import {GGPostgres} from "./GGPostgres";
|
|
3
|
+
import {IsNumber, IsObject, IsString} from "@grest-ts/schema";
|
|
4
|
+
import {GGLocatorKey} from "@grest-ts/locator";
|
|
5
|
+
|
|
6
|
+
const IsPostgresResource = IsObject({
|
|
7
|
+
host: IsString.orUndefined,
|
|
8
|
+
port: IsNumber.orUndefined,
|
|
9
|
+
database: IsString,
|
|
10
|
+
connectionLimit: IsNumber.orUndefined
|
|
11
|
+
});
|
|
12
|
+
export type GGPostgresHostData = typeof IsPostgresResource.infer
|
|
13
|
+
|
|
14
|
+
const IsPostgresUserData = IsObject({
|
|
15
|
+
username: IsString.orUndefined,
|
|
16
|
+
password: IsString.orUndefined
|
|
17
|
+
});
|
|
18
|
+
export type GGPostgresUserData = typeof IsPostgresUserData.infer
|
|
19
|
+
|
|
20
|
+
export class GGPostgresConfig {
|
|
21
|
+
|
|
22
|
+
public readonly name: string;
|
|
23
|
+
public readonly token: GGLocatorKey<GGPostgres>;
|
|
24
|
+
public readonly host: GGResource<GGPostgresHostData>;
|
|
25
|
+
public readonly user: GGSecret<GGPostgresUserData>;
|
|
26
|
+
public readonly schemaFile?: string;
|
|
27
|
+
|
|
28
|
+
constructor(name: string, schemaFile?: string) {
|
|
29
|
+
this.name = name;
|
|
30
|
+
this.token = new GGLocatorKey<GGPostgres>(`Postgres:${name}`);
|
|
31
|
+
this.host = new GGResource(name + "/host", IsPostgresResource, "Postgres host configuration")
|
|
32
|
+
this.user = new GGSecret(name + "/user", IsPostgresUserData, "Postgres user credentials")
|
|
33
|
+
this.schemaFile = schemaFile;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public newPostgresPool() {
|
|
37
|
+
return new GGPostgres(this);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {PoolClient, QueryResult, QueryResultRow} from 'pg';
|
|
2
|
+
import {GGLog} from '@grest-ts/logger';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PostgresConnection - A single connection from the pool.
|
|
6
|
+
*
|
|
7
|
+
* Use this when you need:
|
|
8
|
+
* - Transactions (BEGIN, COMMIT, ROLLBACK)
|
|
9
|
+
* - Multiple queries that must use the same connection
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: Always call release() when done to return the connection to the pool.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const conn = await db.getConnection();
|
|
16
|
+
* try {
|
|
17
|
+
* await conn.beginTransaction();
|
|
18
|
+
* await conn.execute('INSERT INTO users ...', [...]);
|
|
19
|
+
* await conn.execute('INSERT INTO profiles ...', [...]);
|
|
20
|
+
* await conn.commit();
|
|
21
|
+
* } catch (err) {
|
|
22
|
+
* await conn.rollback();
|
|
23
|
+
* throw err;
|
|
24
|
+
* } finally {
|
|
25
|
+
* conn.release();
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class GGPostgresConnection {
|
|
30
|
+
private client: PoolClient;
|
|
31
|
+
private released = false;
|
|
32
|
+
|
|
33
|
+
constructor(client: PoolClient) {
|
|
34
|
+
this.client = client;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute a SELECT query and return rows.
|
|
39
|
+
*/
|
|
40
|
+
async query<T extends QueryResultRow = QueryResultRow>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
41
|
+
this.checkReleased();
|
|
42
|
+
GGLog.debug(this, 'query', {sql, params});
|
|
43
|
+
const result: QueryResult<T> = await this.client.query<T>(sql, params);
|
|
44
|
+
GGLog.debug(this, 'query result', {rowCount: result.rowCount});
|
|
45
|
+
return result.rows;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute an INSERT, UPDATE, or DELETE query.
|
|
50
|
+
*/
|
|
51
|
+
async execute(sql: string, params?: unknown[]): Promise<QueryResult> {
|
|
52
|
+
this.checkReleased();
|
|
53
|
+
GGLog.debug(this, 'execute', {sql, params});
|
|
54
|
+
const result = await this.client.query(sql, params);
|
|
55
|
+
GGLog.debug(this, 'execute result', {rowCount: result.rowCount});
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ==================== Transaction methods ====================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run a callback within a transaction.
|
|
63
|
+
* Automatically commits on success, rolls back on failure.
|
|
64
|
+
*
|
|
65
|
+
* Note: Does NOT release the connection. Call release() when done.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const conn = await db.getConnection();
|
|
70
|
+
* try {
|
|
71
|
+
* const result = await conn.runInTransaction(async () => {
|
|
72
|
+
* await conn.execute('INSERT INTO orders ...', [...]);
|
|
73
|
+
* await conn.execute('UPDATE inventory ...', [...]);
|
|
74
|
+
* return orderId;
|
|
75
|
+
* });
|
|
76
|
+
* } finally {
|
|
77
|
+
* conn.release();
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
async runInTransaction<T>(callback: () => Promise<T>): Promise<T> {
|
|
82
|
+
this.checkReleased();
|
|
83
|
+
await this.client.query('BEGIN');
|
|
84
|
+
GGLog.debug(this, 'beginTransaction');
|
|
85
|
+
try {
|
|
86
|
+
const result = await callback();
|
|
87
|
+
await this.client.query('COMMIT');
|
|
88
|
+
GGLog.debug(this, 'commit');
|
|
89
|
+
return result;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
await this.client.query('ROLLBACK');
|
|
92
|
+
GGLog.debug(this, 'rollback', {error: err});
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Start a transaction.
|
|
99
|
+
*/
|
|
100
|
+
async beginTransaction(): Promise<void> {
|
|
101
|
+
this.checkReleased();
|
|
102
|
+
GGLog.debug(this, 'beginTransaction');
|
|
103
|
+
await this.client.query('BEGIN');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Commit the current transaction.
|
|
108
|
+
*/
|
|
109
|
+
async commit(): Promise<void> {
|
|
110
|
+
this.checkReleased();
|
|
111
|
+
GGLog.debug(this, 'commit');
|
|
112
|
+
await this.client.query('COMMIT');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Rollback the current transaction.
|
|
117
|
+
*/
|
|
118
|
+
async rollback(): Promise<void> {
|
|
119
|
+
this.checkReleased();
|
|
120
|
+
GGLog.debug(this, 'rollback');
|
|
121
|
+
await this.client.query('ROLLBACK');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ==================== Lifecycle ====================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Release this connection back to the pool.
|
|
128
|
+
* MUST be called when done with the connection.
|
|
129
|
+
*/
|
|
130
|
+
release(): void {
|
|
131
|
+
if (!this.released) {
|
|
132
|
+
this.client.release();
|
|
133
|
+
this.released = true;
|
|
134
|
+
GGLog.debug(this, 'released');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if connection has been released.
|
|
140
|
+
*/
|
|
141
|
+
private checkReleased(): void {
|
|
142
|
+
if (this.released) {
|
|
143
|
+
throw new Error('Connection has been released. Cannot perform operations on a released connection.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|