@zintrust/db-mysql 0.1.19 → 0.1.21

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
@@ -12,6 +12,7 @@ export type DatabaseConfig = {
12
12
  export type QueryResult = {
13
13
  rows: Record<string, unknown>[];
14
14
  rowCount: number;
15
+ lastInsertId?: string | number | bigint;
15
16
  };
16
17
  export interface IDatabaseAdapter {
17
18
  connect(): Promise<void>;
@@ -21,6 +22,7 @@ export interface IDatabaseAdapter {
21
22
  ping(): Promise<void>;
22
23
  transaction<T>(callback: (adapter: IDatabaseAdapter) => Promise<T>): Promise<T>;
23
24
  rawQuery<T = unknown>(sql: string, parameters?: unknown[]): Promise<T[]>;
25
+ ensureMigrationsTable?(): Promise<void>;
24
26
  getType(): string;
25
27
  isConnected(): boolean;
26
28
  getPlaceholder(index: number): string;
package/dist/index.js CHANGED
@@ -1,29 +1,113 @@
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
+ const insertId = (typeof maybe.insertId === 'number' ||
50
+ typeof maybe.insertId === 'string' ||
51
+ typeof maybe.insertId === 'bigint') &&
52
+ maybe.insertId !== 0 // Only return if valid ID
53
+ ? maybe.insertId
54
+ : undefined;
55
+ return { rows: [], rowCount: affectedRows, lastInsertId: insertId };
56
+ }
57
+ return { rows: [], rowCount: 0 };
58
+ }
59
+ async function connect(state, config) {
60
+ if (state.connected)
61
+ return;
62
+ try {
63
+ const mysql = await loadMysql();
64
+ const { host, port, database, user, password } = getConnectionParams(config);
65
+ state.pool = mysql.createPool({
66
+ host,
67
+ port,
68
+ database,
69
+ user,
70
+ password,
71
+ waitForConnections: true,
72
+ connectionLimit: 10,
73
+ namedPlaceholders: false,
74
+ });
75
+ // Probe.
76
+ await state.pool.execute('SELECT 1');
77
+ state.connected = true;
78
+ Logger.info(`✓ MySQL connected (${host}:${port})`);
79
+ }
80
+ catch (error) {
81
+ if (isMissingEsmPackage(error, 'mysql2')) {
82
+ throw ErrorFactory.createConfigError("MySQL adapter requires the 'mysql2' package (run `npm install mysql2` or `zin add db:mysql`).");
83
+ }
84
+ throw ErrorFactory.createTryCatchError('Failed to connect to MySQL', error);
85
+ }
86
+ }
87
+ async function disconnect(state) {
88
+ if (!state.connected)
89
+ return;
90
+ const pool = state.pool;
91
+ state.connected = false;
92
+ state.pool = undefined;
93
+ try {
94
+ if (pool !== undefined)
95
+ await pool.end();
96
+ }
97
+ finally {
98
+ Logger.info('✓ MySQL disconnected');
99
+ }
16
100
  }
17
101
  async function rawQuery(state, sql, parameters) {
18
102
  if (!FeatureFlags.isRawQueryEnabled()) {
19
103
  throw ErrorFactory.createConfigError('Raw SQL queries are disabled');
20
104
  }
21
- ensureConnected(state);
105
+ const pool = ensurePool(state);
22
106
  try {
23
107
  Logger.warn(`Raw SQL Query executed: ${sql}`, { parameters });
24
- if (sql.includes('INVALID')) {
25
- throw ErrorFactory.createDatabaseError('Invalid SQL syntax');
26
- }
108
+ const [rows] = await pool.execute(sql, parameters ?? []);
109
+ if (Array.isArray(rows))
110
+ return rows;
27
111
  return [];
28
112
  }
29
113
  catch (error) {
@@ -35,9 +119,15 @@ function createMySqlAdapter(config) {
35
119
  const adapter = {
36
120
  connect: async () => connect(state, config),
37
121
  disconnect: async () => disconnect(state),
38
- query: async () => {
39
- ensureConnected(state);
40
- return { rows: [], rowCount: 0 };
122
+ query: async (sql, parameters) => {
123
+ const pool = ensurePool(state);
124
+ try {
125
+ const [rows] = await pool.execute(sql, parameters);
126
+ return normalizeQueryResult(rows);
127
+ }
128
+ catch (error) {
129
+ throw ErrorFactory.createTryCatchError(`MySQL query failed: ${sql}`, error);
130
+ }
41
131
  },
42
132
  queryOne: async (sql, parameters) => {
43
133
  const result = await adapter.query(sql, parameters);
@@ -47,17 +137,55 @@ function createMySqlAdapter(config) {
47
137
  await adapter.query(QueryBuilder.create('').select('1').toSQL(), []);
48
138
  },
49
139
  transaction: async (callback) => {
50
- ensureConnected(state);
140
+ const pool = ensurePool(state);
141
+ const conn = await pool.getConnection();
142
+ const txAdapter = {
143
+ ...adapter,
144
+ query: async (sql, parameters) => {
145
+ try {
146
+ const [rows] = await conn.execute(sql, parameters);
147
+ return normalizeQueryResult(rows);
148
+ }
149
+ catch (error) {
150
+ throw ErrorFactory.createTryCatchError(`MySQL query failed: ${sql}`, error);
151
+ }
152
+ },
153
+ queryOne: async (sql, parameters) => {
154
+ const res = await txAdapter.query(sql, parameters);
155
+ return res.rows[0] ?? null;
156
+ },
157
+ };
51
158
  try {
52
- await adapter.query('START TRANSACTION', []);
53
- const result = await callback(adapter);
54
- await adapter.query('COMMIT', []);
159
+ await conn.beginTransaction();
160
+ const result = await callback(txAdapter);
161
+ await conn.commit();
55
162
  return result;
56
163
  }
57
164
  catch (error) {
58
- await adapter.query('ROLLBACK', []);
165
+ try {
166
+ await conn.rollback();
167
+ }
168
+ catch {
169
+ // ignore rollback errors
170
+ }
59
171
  throw ErrorFactory.createTryCatchError('MySQL transaction failed', error);
60
172
  }
173
+ finally {
174
+ conn.release();
175
+ }
176
+ },
177
+ ensureMigrationsTable: async () => {
178
+ await adapter.query(`CREATE TABLE IF NOT EXISTS migrations (
179
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
180
+ name VARCHAR(255) NOT NULL,
181
+ scope VARCHAR(255) NOT NULL DEFAULT 'global',
182
+ service VARCHAR(255) NOT NULL DEFAULT '',
183
+ batch INTEGER NOT NULL,
184
+ status VARCHAR(255) NOT NULL,
185
+ applied_at DATETIME NULL,
186
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
187
+ UNIQUE(name, scope, service)
188
+ )`, []);
61
189
  },
62
190
  getType: () => 'mysql',
63
191
  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.21",
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.21"
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
  }