@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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/GGPostgres.d.ts +30 -0
  3. package/dist/src/GGPostgres.d.ts.map +1 -0
  4. package/dist/src/GGPostgres.js +118 -0
  5. package/dist/src/GGPostgres.js.map +1 -0
  6. package/dist/src/GGPostgresConfig.d.ts +26 -0
  7. package/dist/src/GGPostgresConfig.d.ts.map +1 -0
  8. package/dist/src/GGPostgresConfig.js +32 -0
  9. package/dist/src/GGPostgresConfig.js.map +1 -0
  10. package/dist/src/GGPostgresConnection.d.ts +82 -0
  11. package/dist/src/GGPostgresConnection.d.ts.map +1 -0
  12. package/dist/src/GGPostgresConnection.js +135 -0
  13. package/dist/src/GGPostgresConnection.js.map +1 -0
  14. package/dist/src/index-node.d.ts +5 -0
  15. package/dist/src/index-node.d.ts.map +1 -0
  16. package/dist/src/index-node.js +4 -0
  17. package/dist/src/index-node.js.map +1 -0
  18. package/dist/src/tsconfig.json +16 -0
  19. package/dist/testkit/GGPostgresSchemaCloner.d.ts +13 -0
  20. package/dist/testkit/GGPostgresSchemaCloner.d.ts.map +1 -0
  21. package/dist/testkit/GGPostgresSchemaCloner.js +51 -0
  22. package/dist/testkit/GGPostgresSchemaCloner.js.map +1 -0
  23. package/dist/testkit/GGPostgresSchemaOperations.d.ts +20 -0
  24. package/dist/testkit/GGPostgresSchemaOperations.d.ts.map +1 -0
  25. package/dist/testkit/GGPostgresSchemaOperations.js +111 -0
  26. package/dist/testkit/GGPostgresSchemaOperations.js.map +1 -0
  27. package/dist/testkit/GGPostgresTestMethods.d.ts +44 -0
  28. package/dist/testkit/GGPostgresTestMethods.d.ts.map +1 -0
  29. package/dist/testkit/GGPostgresTestMethods.js +103 -0
  30. package/dist/testkit/GGPostgresTestMethods.js.map +1 -0
  31. package/dist/testkit/index-testkit.d.ts +3 -0
  32. package/dist/testkit/index-testkit.d.ts.map +1 -0
  33. package/dist/testkit/index-testkit.js +3 -0
  34. package/dist/testkit/index-testkit.js.map +1 -0
  35. package/dist/tsconfig.publish.tsbuildinfo +1 -0
  36. package/package.json +62 -0
  37. package/src/GGPostgres.ts +138 -0
  38. package/src/GGPostgresConfig.ts +39 -0
  39. package/src/GGPostgresConnection.ts +146 -0
  40. package/src/index-node.ts +4 -0
  41. 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
+ }
@@ -0,0 +1,4 @@
1
+ export * from './GGPostgres';
2
+ export * from "./GGPostgresConfig";
3
+ export * from './GGPostgresConnection';
4
+ export type {QueryResult, QueryResultRow} from 'pg';
@@ -0,0 +1,16 @@
1
+ {
2
+ "//": "THIS FILE IS GENERATED - DO NOT EDIT",
3
+ "extends": "../../../../tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "rootDir": ".",
6
+ "lib": [
7
+ "ES2022"
8
+ ],
9
+ "types": [
10
+ "node"
11
+ ]
12
+ },
13
+ "include": [
14
+ "**/*"
15
+ ]
16
+ }