@zintrust/db-mysql 0.1.19 → 0.1.20

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 CHANGED
@@ -34,3 +34,7 @@ DB_CONNECTION=mysql
34
34
 
35
35
  - https://zintrust.com/adapters
36
36
  - https://zintrust.com/database-advanced
37
+
38
+ ## License
39
+
40
+ This package and its dependencies are MIT licensed, permitting free commercial use.
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export interface IDatabaseAdapter {
21
21
  ping(): Promise<void>;
22
22
  transaction<T>(callback: (adapter: IDatabaseAdapter) => Promise<T>): Promise<T>;
23
23
  rawQuery<T = unknown>(sql: string, parameters?: unknown[]): Promise<T[]>;
24
+ ensureMigrationsTable?(): Promise<void>;
24
25
  getType(): string;
25
26
  isConnected(): boolean;
26
27
  getPlaceholder(index: number): string;
package/dist/index.js CHANGED
@@ -1,29 +1,107 @@
1
1
  import { ErrorFactory, FeatureFlags, Logger, QueryBuilder } from '@zintrust/core';
2
- function connect(state, config) {
3
- if (config.host === 'error') {
4
- throw ErrorFactory.createConnectionError('Failed to connect to MySQL: Error: Connection failed');
2
+ function isMissingEsmPackage(error, packageName) {
3
+ if (error === null || typeof error !== 'object')
4
+ return false;
5
+ const maybe = error;
6
+ if (maybe.code === 'ERR_MODULE_NOT_FOUND') {
7
+ return typeof maybe.message === 'string' && maybe.message.includes(packageName);
5
8
  }
6
- state.connected = true;
7
- Logger.info(`✓ MySQL connected (${config.host}:${config.port})`);
9
+ if (typeof maybe.message === 'string') {
10
+ return (maybe.message.includes(`Cannot find package '${packageName}'`) ||
11
+ maybe.message.includes(`Cannot find module '${packageName}'`));
12
+ }
13
+ return false;
8
14
  }
9
- function disconnect(state) {
10
- state.connected = false;
11
- Logger.info('✓ MySQL disconnected');
15
+ async function loadMysql() {
16
+ return (await import('mysql2/promise'));
12
17
  }
13
- function ensureConnected(state) {
14
- if (!state.connected)
18
+ function getConnectionParams(config) {
19
+ return {
20
+ host: config.host ?? 'localhost',
21
+ port: config.port ?? 3306,
22
+ database: config.database ?? 'mysql',
23
+ user: config.username ?? 'root',
24
+ password: config.password ?? '',
25
+ };
26
+ }
27
+ function ensurePool(state) {
28
+ if (!state.connected || state.pool === undefined) {
15
29
  throw ErrorFactory.createConnectionError('Database not connected');
30
+ }
31
+ return state.pool;
32
+ }
33
+ function normalizeQueryResult(raw) {
34
+ // mysql2/promise returns:
35
+ // - SELECT: RowDataPacket[]
36
+ // - INSERT/UPDATE: ResultSetHeader
37
+ // We normalize to the framework's { rows, rowCount }.
38
+ if (Array.isArray(raw)) {
39
+ return {
40
+ rows: raw,
41
+ rowCount: raw.length,
42
+ };
43
+ }
44
+ if (raw !== null && typeof raw === 'object') {
45
+ const maybe = raw;
46
+ const affectedRows = typeof maybe.affectedRows === 'number' && Number.isFinite(maybe.affectedRows)
47
+ ? maybe.affectedRows
48
+ : 0;
49
+ return { rows: [], rowCount: affectedRows };
50
+ }
51
+ return { rows: [], rowCount: 0 };
52
+ }
53
+ async function connect(state, config) {
54
+ if (state.connected)
55
+ return;
56
+ try {
57
+ const mysql = await loadMysql();
58
+ const { host, port, database, user, password } = getConnectionParams(config);
59
+ state.pool = mysql.createPool({
60
+ host,
61
+ port,
62
+ database,
63
+ user,
64
+ password,
65
+ waitForConnections: true,
66
+ connectionLimit: 10,
67
+ namedPlaceholders: false,
68
+ });
69
+ // Probe.
70
+ await state.pool.execute('SELECT 1');
71
+ state.connected = true;
72
+ Logger.info(`✓ MySQL connected (${host}:${port})`);
73
+ }
74
+ catch (error) {
75
+ if (isMissingEsmPackage(error, 'mysql2')) {
76
+ throw ErrorFactory.createConfigError("MySQL adapter requires the 'mysql2' package (run `npm install mysql2` or `zin add db:mysql`).");
77
+ }
78
+ throw ErrorFactory.createTryCatchError('Failed to connect to MySQL', error);
79
+ }
80
+ }
81
+ async function disconnect(state) {
82
+ if (!state.connected)
83
+ return;
84
+ const pool = state.pool;
85
+ state.connected = false;
86
+ state.pool = undefined;
87
+ try {
88
+ if (pool !== undefined)
89
+ await pool.end();
90
+ }
91
+ finally {
92
+ Logger.info('✓ MySQL disconnected');
93
+ }
16
94
  }
17
95
  async function rawQuery(state, sql, parameters) {
18
96
  if (!FeatureFlags.isRawQueryEnabled()) {
19
97
  throw ErrorFactory.createConfigError('Raw SQL queries are disabled');
20
98
  }
21
- ensureConnected(state);
99
+ const pool = ensurePool(state);
22
100
  try {
23
101
  Logger.warn(`Raw SQL Query executed: ${sql}`, { parameters });
24
- if (sql.includes('INVALID')) {
25
- throw ErrorFactory.createDatabaseError('Invalid SQL syntax');
26
- }
102
+ const [rows] = await pool.execute(sql, parameters ?? []);
103
+ if (Array.isArray(rows))
104
+ return rows;
27
105
  return [];
28
106
  }
29
107
  catch (error) {
@@ -35,9 +113,15 @@ function createMySqlAdapter(config) {
35
113
  const adapter = {
36
114
  connect: async () => connect(state, config),
37
115
  disconnect: async () => disconnect(state),
38
- query: async () => {
39
- ensureConnected(state);
40
- return { rows: [], rowCount: 0 };
116
+ query: async (sql, parameters) => {
117
+ const pool = ensurePool(state);
118
+ try {
119
+ const [rows] = await pool.execute(sql, parameters);
120
+ return normalizeQueryResult(rows);
121
+ }
122
+ catch (error) {
123
+ throw ErrorFactory.createTryCatchError(`MySQL query failed: ${sql}`, error);
124
+ }
41
125
  },
42
126
  queryOne: async (sql, parameters) => {
43
127
  const result = await adapter.query(sql, parameters);
@@ -47,17 +131,55 @@ function createMySqlAdapter(config) {
47
131
  await adapter.query(QueryBuilder.create('').select('1').toSQL(), []);
48
132
  },
49
133
  transaction: async (callback) => {
50
- ensureConnected(state);
134
+ const pool = ensurePool(state);
135
+ const conn = await pool.getConnection();
136
+ const txAdapter = {
137
+ ...adapter,
138
+ query: async (sql, parameters) => {
139
+ try {
140
+ const [rows] = await conn.execute(sql, parameters);
141
+ return normalizeQueryResult(rows);
142
+ }
143
+ catch (error) {
144
+ throw ErrorFactory.createTryCatchError(`MySQL query failed: ${sql}`, error);
145
+ }
146
+ },
147
+ queryOne: async (sql, parameters) => {
148
+ const res = await txAdapter.query(sql, parameters);
149
+ return res.rows[0] ?? null;
150
+ },
151
+ };
51
152
  try {
52
- await adapter.query('START TRANSACTION', []);
53
- const result = await callback(adapter);
54
- await adapter.query('COMMIT', []);
153
+ await conn.beginTransaction();
154
+ const result = await callback(txAdapter);
155
+ await conn.commit();
55
156
  return result;
56
157
  }
57
158
  catch (error) {
58
- await adapter.query('ROLLBACK', []);
159
+ try {
160
+ await conn.rollback();
161
+ }
162
+ catch {
163
+ // ignore rollback errors
164
+ }
59
165
  throw ErrorFactory.createTryCatchError('MySQL transaction failed', error);
60
166
  }
167
+ finally {
168
+ conn.release();
169
+ }
170
+ },
171
+ ensureMigrationsTable: async () => {
172
+ await adapter.query(`CREATE TABLE IF NOT EXISTS migrations (
173
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
174
+ name VARCHAR(255) NOT NULL,
175
+ scope VARCHAR(255) NOT NULL DEFAULT 'global',
176
+ service VARCHAR(255) NOT NULL DEFAULT '',
177
+ batch INTEGER NOT NULL,
178
+ status VARCHAR(255) NOT NULL,
179
+ applied_at DATETIME NULL,
180
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
181
+ UNIQUE(name, scope, service)
182
+ )`, []);
61
183
  },
62
184
  getType: () => 'mysql',
63
185
  isConnected: () => state.connected,
package/dist/register.js CHANGED
@@ -1,8 +1,32 @@
1
- import { MySQLAdapter } from './index.js';
1
+ // Prefer production build output shape first.
2
+ // Dev (tsx) fallback when running directly from src.
3
+ const adapterModule = (await (async () => {
4
+ try {
5
+ return (await import('./index.js'));
6
+ }
7
+ catch {
8
+ return (await import('./index'));
9
+ }
10
+ })());
2
11
  export function registerMySqlAdapter(registry) {
3
- registry.register('mysql', (config) => MySQLAdapter.create(config));
12
+ registry.register('mysql', (config) => adapterModule.MySQLAdapter.create(config));
4
13
  }
5
- const core = (await import('@zintrust/core'));
6
- if (core.DatabaseAdapterRegistry !== undefined) {
7
- registerMySqlAdapter(core.DatabaseAdapterRegistry);
14
+ const globalWithRegistry = globalThis;
15
+ const globalRegistry = globalWithRegistry.__zintrust_db_adapter_registry__ ??
16
+ (globalWithRegistry.__zintrust_db_adapter_registry__ = new Map());
17
+ registerMySqlAdapter({
18
+ register: (driver, factory) => {
19
+ globalRegistry.set(driver, factory);
20
+ },
21
+ });
22
+ // Side-effect registration when used as a published package.
23
+ // In monorepo/dev setups, @zintrust/core may not be resolvable; ignore if missing.
24
+ try {
25
+ const core = (await import('@zintrust/core'));
26
+ if (core.DatabaseAdapterRegistry !== undefined) {
27
+ registerMySqlAdapter(core.DatabaseAdapterRegistry);
28
+ }
29
+ }
30
+ catch {
31
+ // no-op
8
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/db-mysql",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "node": ">=20.0.0"
23
23
  },
24
24
  "peerDependencies": {
25
- "@zintrust/core": "^0.1.19"
25
+ "@zintrust/core": "^0.1.20"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"
@@ -30,5 +30,8 @@
30
30
  "scripts": {
31
31
  "build": "tsc -p tsconfig.json",
32
32
  "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "mysql2": "^3.16.0"
33
36
  }
34
37
  }