@grest-ts/db-mysql 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/GGMysql.d.ts +30 -0
- package/dist/src/GGMysql.d.ts.map +1 -0
- package/dist/src/GGMysql.js +122 -0
- package/dist/src/GGMysql.js.map +1 -0
- package/dist/src/GGMysqlConfig.d.ts +26 -0
- package/dist/src/GGMysqlConfig.d.ts.map +1 -0
- package/dist/src/GGMysqlConfig.js +32 -0
- package/dist/src/GGMysqlConfig.js.map +1 -0
- package/dist/src/GGMysqlConnection.d.ts +82 -0
- package/dist/src/GGMysqlConnection.d.ts.map +1 -0
- package/dist/src/GGMysqlConnection.js +147 -0
- package/dist/src/GGMysqlConnection.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/GGMysqlSchemaCloner.d.ts +13 -0
- package/dist/testkit/GGMysqlSchemaCloner.d.ts.map +1 -0
- package/dist/testkit/GGMysqlSchemaCloner.js +51 -0
- package/dist/testkit/GGMysqlSchemaCloner.js.map +1 -0
- package/dist/testkit/GGMysqlSchemaOperations.d.ts +20 -0
- package/dist/testkit/GGMysqlSchemaOperations.d.ts.map +1 -0
- package/dist/testkit/GGMysqlSchemaOperations.js +95 -0
- package/dist/testkit/GGMysqlSchemaOperations.js.map +1 -0
- package/dist/testkit/GGMysqlTestMethods.d.ts +47 -0
- package/dist/testkit/GGMysqlTestMethods.d.ts.map +1 -0
- package/dist/testkit/GGMysqlTestMethods.js +106 -0
- package/dist/testkit/GGMysqlTestMethods.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 +61 -0
- package/src/GGMysql.ts +142 -0
- package/src/GGMysqlConfig.ts +39 -0
- package/src/GGMysqlConnection.ts +156 -0
- package/src/index-node.ts +4 -0
- package/src/tsconfig.json +16 -0
package/src/GGMysql.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import mysql, {Pool, ResultSetHeader, RowDataPacket} from 'mysql2/promise';
|
|
2
|
+
import {GGLocator, GGLocatorServiceType} from '@grest-ts/locator';
|
|
3
|
+
import {GGLog} from '@grest-ts/logger';
|
|
4
|
+
import {GGMysqlConnection} from './GGMysqlConnection';
|
|
5
|
+
import type {GGMysqlConfig} from "./GGMysqlConfig";
|
|
6
|
+
|
|
7
|
+
export class GGMysql {
|
|
8
|
+
|
|
9
|
+
private readonly config: GGMysqlConfig;
|
|
10
|
+
|
|
11
|
+
private started = false;
|
|
12
|
+
|
|
13
|
+
private pool: Pool | undefined = null;
|
|
14
|
+
private unwatchHost: (() => void) | undefined = undefined;
|
|
15
|
+
private unwatchUser: (() => void) | undefined = undefined;
|
|
16
|
+
|
|
17
|
+
constructor(config: GGMysqlConfig) {
|
|
18
|
+
this.config = config
|
|
19
|
+
|
|
20
|
+
this.unwatchHost = this.config.host.watch(() => this.connect().catch(() => {}));
|
|
21
|
+
this.unwatchUser = this.config.user.watch(() => this.connect().catch(() => {}));
|
|
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: Pool = mysql.createPool({
|
|
40
|
+
host: config.host ?? "localhost",
|
|
41
|
+
port: config.port ?? 3306,
|
|
42
|
+
user: user.username,
|
|
43
|
+
password: user.password,
|
|
44
|
+
database: config.database,
|
|
45
|
+
connectionLimit: connectionsLimit,
|
|
46
|
+
waitForConnections: true,
|
|
47
|
+
queueLimit: connectionsLimit * 2,
|
|
48
|
+
decimalNumbers: true,
|
|
49
|
+
dateStrings: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const conn = await newPool.getConnection();
|
|
54
|
+
await conn.ping();
|
|
55
|
+
conn.release();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
GGLog.critical(this, 'Failed to connect to database!', {
|
|
58
|
+
database: config.database,
|
|
59
|
+
host: config.host,
|
|
60
|
+
error: err instanceof Error ? err.message : String(err)
|
|
61
|
+
});
|
|
62
|
+
await newPool.end();
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (this.pool) {
|
|
67
|
+
this.pool.end().catch(err => {
|
|
68
|
+
GGLog.warn(this, 'Config change: error closing old pool', {
|
|
69
|
+
error: err instanceof Error ? err.message : String(err)
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.pool = newPool;
|
|
75
|
+
|
|
76
|
+
GGLog.info(this, 'Mysql connected!', {database: config.database});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async start(): Promise<void> {
|
|
80
|
+
this.started = true;
|
|
81
|
+
await this.connect();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async teardown(): Promise<void> {
|
|
85
|
+
this.unwatchHost();
|
|
86
|
+
this.unwatchHost = undefined;
|
|
87
|
+
this.unwatchUser();
|
|
88
|
+
this.unwatchUser = undefined;
|
|
89
|
+
if (this.pool) {
|
|
90
|
+
await this.pool.end();
|
|
91
|
+
this.pool = undefined;
|
|
92
|
+
GGLog.debug(this, 'disconnected');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private getPool(): Pool {
|
|
97
|
+
if (!this.pool) {
|
|
98
|
+
throw new Error(`Mysql '${this.config.name}' not connected. Are you calling this before runtime.start()?`);
|
|
99
|
+
}
|
|
100
|
+
return this.pool;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public async query<T extends RowDataPacket[]>(sql: string, params?: unknown[]): Promise<T> {
|
|
104
|
+
GGLog.debug(this, 'query', {sql, params});
|
|
105
|
+
const [rows] = await this.getPool().query<T>(sql, params);
|
|
106
|
+
GGLog.debug(this, 'query result', {rowCount: rows.length});
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async execute(sql: string, params?: unknown[]): Promise<ResultSetHeader> {
|
|
111
|
+
GGLog.debug(this, 'execute', {sql, params});
|
|
112
|
+
const [result] = await this.getPool().execute<ResultSetHeader>(sql, params);
|
|
113
|
+
GGLog.debug(this, 'execute result', {affectedRows: result.affectedRows, insertId: result.insertId});
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ==================== Connection for transactions ====================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get a dedicated connection from the pool.
|
|
121
|
+
* Use this for transactions or when you need multiple queries on the same connection.
|
|
122
|
+
*
|
|
123
|
+
* IMPORTANT: Always call release() on the connection when done.
|
|
124
|
+
*/
|
|
125
|
+
public async getConnection(): Promise<GGMysqlConnection> {
|
|
126
|
+
const poolConn = await this.getPool().getConnection();
|
|
127
|
+
return new GGMysqlConnection(poolConn);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run a callback within a transaction.
|
|
132
|
+
* Automatically handles connection lifecycle, commits on success, rolls back on failure.
|
|
133
|
+
*/
|
|
134
|
+
public async runInTransaction<T>(callback: (conn: GGMysqlConnection) => Promise<T>): Promise<T> {
|
|
135
|
+
const conn = await this.getConnection();
|
|
136
|
+
try {
|
|
137
|
+
return await conn.runInTransaction(() => callback(conn));
|
|
138
|
+
} finally {
|
|
139
|
+
conn.release();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {GGResource, GGSecret} from "@grest-ts/config";
|
|
2
|
+
import {GGMysql} from "./GGMysql";
|
|
3
|
+
import {IsNumber, IsObject, IsString} from "@grest-ts/schema";
|
|
4
|
+
import {GGLocatorKey} from "@grest-ts/locator";
|
|
5
|
+
|
|
6
|
+
const IsMysqlResource = IsObject({
|
|
7
|
+
host: IsString.orUndefined,
|
|
8
|
+
port: IsNumber.orUndefined,
|
|
9
|
+
database: IsString,
|
|
10
|
+
connectionLimit: IsNumber.orUndefined
|
|
11
|
+
});
|
|
12
|
+
export type GGMysqlHostData = typeof IsMysqlResource.infer
|
|
13
|
+
|
|
14
|
+
const IsMysqlUserData = IsObject({
|
|
15
|
+
username: IsString.orUndefined,
|
|
16
|
+
password: IsString.orUndefined
|
|
17
|
+
});
|
|
18
|
+
export type GGMysqlUserData = typeof IsMysqlUserData.infer
|
|
19
|
+
|
|
20
|
+
export class GGMysqlConfig {
|
|
21
|
+
|
|
22
|
+
public readonly name: string;
|
|
23
|
+
public readonly token: GGLocatorKey<GGMysql>;
|
|
24
|
+
public readonly host: GGResource<GGMysqlHostData>;
|
|
25
|
+
public readonly user: GGSecret<GGMysqlUserData>;
|
|
26
|
+
public readonly schemaFile?: string;
|
|
27
|
+
|
|
28
|
+
constructor(name: string, schemaFile?: string) {
|
|
29
|
+
this.name = name;
|
|
30
|
+
this.token = new GGLocatorKey<GGMysql>(`Mysql:${name}`);
|
|
31
|
+
this.host = new GGResource(name + "/host", IsMysqlResource, "Mysql host configuration")
|
|
32
|
+
this.user = new GGSecret(name + "/user", IsMysqlUserData, "Mysql user credentials")
|
|
33
|
+
this.schemaFile = schemaFile;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public newMysqlPool() {
|
|
37
|
+
return new GGMysql(this);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { PoolConnection, RowDataPacket, ResultSetHeader } from 'mysql2/promise';
|
|
2
|
+
import { GGLog } from '@grest-ts/logger';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MysqlConnection - 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 GGMysqlConnection {
|
|
30
|
+
private conn: PoolConnection;
|
|
31
|
+
private released = false;
|
|
32
|
+
|
|
33
|
+
constructor(conn: PoolConnection) {
|
|
34
|
+
this.conn = conn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute a SELECT query and return rows.
|
|
39
|
+
*/
|
|
40
|
+
async query<T extends RowDataPacket[]>(sql: string, params?: unknown[]): Promise<T> {
|
|
41
|
+
this.checkReleased();
|
|
42
|
+
GGLog.debug(this, 'query', { sql, params });
|
|
43
|
+
try {
|
|
44
|
+
const [rows] = await this.conn.query<T>(sql, params);
|
|
45
|
+
GGLog.debug(this, 'query result', { rowCount: rows.length });
|
|
46
|
+
return rows;
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
const msg = err?.message || String(err);
|
|
49
|
+
throw new Error(`SQL query failed: ${msg}\n Query: ${sql}` + (params?.length ? `\n Params: ${JSON.stringify(params)}` : ''));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Execute an INSERT, UPDATE, or DELETE query.
|
|
55
|
+
*/
|
|
56
|
+
async execute(sql: string, params?: unknown[]): Promise<ResultSetHeader> {
|
|
57
|
+
this.checkReleased();
|
|
58
|
+
GGLog.debug(this, 'execute', { sql, params });
|
|
59
|
+
try {
|
|
60
|
+
const [result] = await this.conn.execute<ResultSetHeader>(sql, params);
|
|
61
|
+
GGLog.debug(this, 'execute result', { affectedRows: result.affectedRows, insertId: result.insertId });
|
|
62
|
+
return result;
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
const msg = err?.message || String(err);
|
|
65
|
+
throw new Error(`SQL execute failed: ${msg}\n Query: ${sql}` + (params?.length ? `\n Params: ${JSON.stringify(params)}` : ''));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ==================== Transaction methods ====================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run a callback within a transaction.
|
|
73
|
+
* Automatically commits on success, rolls back on failure.
|
|
74
|
+
*
|
|
75
|
+
* Note: Does NOT release the connection. Call release() when done.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const conn = await db.getConnection();
|
|
80
|
+
* try {
|
|
81
|
+
* const result = await conn.runInTransaction(async () => {
|
|
82
|
+
* await conn.execute('INSERT INTO orders ...', [...]);
|
|
83
|
+
* await conn.execute('UPDATE inventory ...', [...]);
|
|
84
|
+
* return orderId;
|
|
85
|
+
* });
|
|
86
|
+
* } finally {
|
|
87
|
+
* conn.release();
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
async runInTransaction<T>(callback: () => Promise<T>): Promise<T> {
|
|
92
|
+
this.checkReleased();
|
|
93
|
+
await this.conn.beginTransaction();
|
|
94
|
+
GGLog.debug(this, 'beginTransaction');
|
|
95
|
+
try {
|
|
96
|
+
const result = await callback();
|
|
97
|
+
await this.conn.commit();
|
|
98
|
+
GGLog.debug(this, 'commit');
|
|
99
|
+
return result;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
await this.conn.rollback();
|
|
102
|
+
GGLog.debug(this, 'rollback', { error: err });
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start a transaction.
|
|
109
|
+
*/
|
|
110
|
+
async beginTransaction(): Promise<void> {
|
|
111
|
+
this.checkReleased();
|
|
112
|
+
GGLog.debug(this, 'beginTransaction');
|
|
113
|
+
await this.conn.beginTransaction();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Commit the current transaction.
|
|
118
|
+
*/
|
|
119
|
+
async commit(): Promise<void> {
|
|
120
|
+
this.checkReleased();
|
|
121
|
+
GGLog.debug(this, 'commit');
|
|
122
|
+
await this.conn.commit();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Rollback the current transaction.
|
|
127
|
+
*/
|
|
128
|
+
async rollback(): Promise<void> {
|
|
129
|
+
this.checkReleased();
|
|
130
|
+
GGLog.debug(this, 'rollback');
|
|
131
|
+
await this.conn.rollback();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ==================== Lifecycle ====================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Release this connection back to the pool.
|
|
138
|
+
* MUST be called when done with the connection.
|
|
139
|
+
*/
|
|
140
|
+
release(): void {
|
|
141
|
+
if (!this.released) {
|
|
142
|
+
this.conn.release();
|
|
143
|
+
this.released = true;
|
|
144
|
+
GGLog.debug(this, 'released');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if connection has been released.
|
|
150
|
+
*/
|
|
151
|
+
private checkReleased(): void {
|
|
152
|
+
if (this.released) {
|
|
153
|
+
throw new Error('Connection has been released. Cannot perform operations on a released connection.');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|