@axiosleo/orm-mysql 0.14.4 → 0.15.0

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
@@ -477,6 +477,49 @@ const hanlder = new QueryHandler(conn, {
477
477
  });
478
478
  ```
479
479
 
480
+ ## AI Skills
481
+
482
+ This project ships with AI Skills that teach AI coding assistants how to use this library correctly. You can install them with a single command:
483
+
484
+ ```bash
485
+ # Cursor
486
+ npx @axiosleo/orm-mysql skills --install=cursor
487
+
488
+ # Claude Code
489
+ npx @axiosleo/orm-mysql skills --install=claude
490
+
491
+ # Windsurf
492
+ npx @axiosleo/orm-mysql skills --install=windsurf
493
+ ```
494
+
495
+ The command detects the locally installed version of `@axiosleo/orm-mysql` in your `node_modules/` and copies the matching skill files. If the package is not installed locally, it will use the bundled skills and remind you to run `npm install @axiosleo/orm-mysql`.
496
+
497
+ To uninstall:
498
+
499
+ ```bash
500
+ npx @axiosleo/orm-mysql skills --uninstall=cursor
501
+ npx @axiosleo/orm-mysql skills --uninstall=claude
502
+ npx @axiosleo/orm-mysql skills --uninstall=windsurf
503
+ ```
504
+
505
+ ### Skill Files
506
+
507
+ | File | Content |
508
+ |------|---------|
509
+ | `SKILL.md` | Setup, class hierarchy, quick start |
510
+ | `query-building.md` | table, attr, join, orderBy, limit, groupBy |
511
+ | `where-conditions.md` | where, whereIn, whereLike, whereBetween |
512
+ | `crud-operations.md` | select, find, insert, update, delete, incrBy |
513
+ | `transactions.md` | beginTransaction, isolation levels, row locking |
514
+
515
+ ### Manual Installation
516
+
517
+ For other AI tools, copy the skill files from `node_modules/@axiosleo/orm-mysql/skills/` into your tool's custom instructions directory:
518
+
519
+ ```bash
520
+ cp -r node_modules/@axiosleo/orm-mysql/skills/ .your-tool/skills/orm-mysql-usage/
521
+ ```
522
+
480
523
  ## License
481
524
 
482
525
  This project is open-sourced software licensed under [MIT](LICENSE).
package/bin/orm-mysql.js CHANGED
@@ -9,7 +9,7 @@ const app = new App({
9
9
  name: 'MySQL ORM CLI',
10
10
  desc: 'migrate, model, seed, etc.',
11
11
  bin: 'orm-mysql',
12
- version: '0.14.4',
12
+ version: '0.15.0',
13
13
  commands_dir: path.join(__dirname, '../commands'),
14
14
  });
15
15
 
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { Command, printer } = require('@axiosleo/cli-tool');
6
+ const { _exists, _copy, _read, _read_json, _mkdir } = require('@axiosleo/cli-tool/src/helper/fs');
7
+
8
+ const SKILL_FILES = [
9
+ 'SKILL.md',
10
+ 'query-building.md',
11
+ 'where-conditions.md',
12
+ 'crud-operations.md',
13
+ 'transactions.md',
14
+ ];
15
+
16
+ const SUPPORTED_TARGETS = ['cursor', 'claude', 'windsurf'];
17
+
18
+ const PKG_NAME = '@axiosleo/orm-mysql';
19
+
20
+ function getTargetDir(cwd, target) {
21
+ const map = {
22
+ cursor: path.join(cwd, '.cursor', 'skills', 'orm-mysql-usage'),
23
+ windsurf: path.join(cwd, '.windsurf', 'skills', 'orm-mysql-usage'),
24
+ };
25
+ return map[target] || null;
26
+ }
27
+
28
+ async function resolveSkillsSource(cwd) {
29
+ const localPkgJson = path.join(cwd, 'node_modules', PKG_NAME, 'package.json');
30
+ let localPkgFound = false;
31
+ let localVersion = null;
32
+
33
+ if (await _exists(localPkgJson)) {
34
+ localPkgFound = true;
35
+ const pkg = await _read_json(localPkgJson);
36
+ localVersion = pkg.version;
37
+ const skillsDir = path.join(cwd, 'node_modules', PKG_NAME, 'skills');
38
+ if (await _exists(skillsDir)) {
39
+ return { dir: skillsDir, version: localVersion, local: true, outdated: false };
40
+ }
41
+ }
42
+
43
+ const fallbackDir = path.join(__dirname, '..', 'skills');
44
+ if (await _exists(fallbackDir)) {
45
+ const fallbackPkg = path.join(__dirname, '..', 'package.json');
46
+ let version = 'unknown';
47
+ if (await _exists(fallbackPkg)) {
48
+ const pkg = await _read_json(fallbackPkg);
49
+ version = pkg.version;
50
+ }
51
+ return {
52
+ dir: fallbackDir,
53
+ version,
54
+ local: false,
55
+ outdated: localPkgFound,
56
+ localVersion,
57
+ };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ class SkillsCommand extends Command {
63
+ constructor() {
64
+ super({
65
+ name: 'skills',
66
+ desc: 'Install or uninstall AI skills for coding assistants (Cursor, Claude Code, Windsurf)',
67
+ });
68
+ this.addOption('install', 'i', 'Install skills for target tool (cursor, claude, windsurf)', 'optional', '');
69
+ this.addOption('uninstall', 'u', 'Uninstall skills for target tool (cursor, claude, windsurf)', 'optional', '');
70
+ }
71
+
72
+ async exec(args, options) {
73
+ const install = options.install;
74
+ const uninstall = options.uninstall;
75
+
76
+ if (!install && !uninstall) {
77
+ this.printUsage();
78
+ return;
79
+ }
80
+
81
+ if (install) {
82
+ await this.install(install);
83
+ } else if (uninstall) {
84
+ await this.uninstall(uninstall);
85
+ }
86
+ }
87
+
88
+ printUsage() {
89
+ printer.println();
90
+ printer.println('Usage:');
91
+ printer.println(' orm-mysql skills --install=<target> Install AI skills');
92
+ printer.println(' orm-mysql skills --uninstall=<target> Uninstall AI skills');
93
+ printer.println();
94
+ printer.println('Supported targets: ' + SUPPORTED_TARGETS.join(', '));
95
+ printer.println();
96
+ printer.println('Examples:');
97
+ printer.println(' npx @axiosleo/orm-mysql skills --install=cursor');
98
+ printer.println(' npx @axiosleo/orm-mysql skills --install=claude');
99
+ printer.println(' npx @axiosleo/orm-mysql skills --uninstall=cursor');
100
+ printer.println();
101
+ }
102
+
103
+ async install(target) {
104
+ if (!SUPPORTED_TARGETS.includes(target)) {
105
+ printer.error(`Unsupported target: "${target}". Supported targets: ${SUPPORTED_TARGETS.join(', ')}`);
106
+ return;
107
+ }
108
+
109
+ const cwd = process.cwd();
110
+ const source = await resolveSkillsSource(cwd);
111
+
112
+ if (!source) {
113
+ printer.error('Could not find skills files. Please reinstall @axiosleo/orm-mysql.');
114
+ return;
115
+ }
116
+
117
+ if (source.local) {
118
+ printer.info(`Found ${PKG_NAME}@${source.version} in node_modules`);
119
+ } else if (source.outdated) {
120
+ printer.warning(`${PKG_NAME}@${source.localVersion} is installed locally but does not include skills files.`);
121
+ printer.warning('Skills files are available since v0.15.0. Please update:');
122
+ printer.warning(` npm install ${PKG_NAME}@latest`);
123
+ printer.println();
124
+ printer.info(`Using skills from npx ${PKG_NAME}@${source.version} instead.`);
125
+ } else {
126
+ printer.warning(`${PKG_NAME} is not installed locally in this project.`);
127
+ printer.warning(`Consider running: npm install ${PKG_NAME}`);
128
+ printer.println();
129
+ }
130
+
131
+ printer.info(`Installing skills for ${target} from ${PKG_NAME}@${source.version}...`);
132
+
133
+ if (target === 'claude') {
134
+ await this.installClaude(cwd, source);
135
+ } else {
136
+ await this.installCopyTarget(cwd, target, source);
137
+ }
138
+ }
139
+
140
+ async installCopyTarget(cwd, target, source) {
141
+ const targetDir = getTargetDir(cwd, target);
142
+ await _mkdir(targetDir);
143
+ await _copy(source.dir, targetDir, true);
144
+ printer.success(`Skills installed to ${path.relative(cwd, targetDir)}/`);
145
+ printer.println();
146
+ printer.println('Files installed:');
147
+ for (const file of SKILL_FILES) {
148
+ printer.println(` - ${file}`);
149
+ }
150
+ printer.println();
151
+ }
152
+
153
+ async installClaude(cwd, source) {
154
+ const claudeFile = path.join(cwd, 'CLAUDE.md');
155
+ const header = `<!-- ${PKG_NAME}@${source.version} skills -->\n`;
156
+ const separator = '\n---\n\n';
157
+
158
+ let content = header;
159
+ for (const file of SKILL_FILES) {
160
+ const filePath = path.join(source.dir, file);
161
+ if (await _exists(filePath)) {
162
+ const fileContent = await _read(filePath);
163
+ content += separator + fileContent.trim() + '\n';
164
+ }
165
+ }
166
+
167
+ if (await _exists(claudeFile)) {
168
+ const existing = await _read(claudeFile);
169
+ if (existing.includes(`<!-- ${PKG_NAME}`) && existing.includes('skills -->')) {
170
+ printer.warning('CLAUDE.md already contains @axiosleo/orm-mysql skills.');
171
+ printer.warning('Please remove the existing skills section first, or run:');
172
+ printer.warning(' npx @axiosleo/orm-mysql skills --uninstall=claude');
173
+ return;
174
+ }
175
+ }
176
+
177
+ fs.appendFileSync(claudeFile, '\n' + content);
178
+ printer.success(`Skills appended to ${path.relative(cwd, claudeFile)}`);
179
+ printer.println();
180
+ }
181
+
182
+ async uninstall(target) {
183
+ if (!SUPPORTED_TARGETS.includes(target)) {
184
+ printer.error(`Unsupported target: "${target}". Supported targets: ${SUPPORTED_TARGETS.join(', ')}`);
185
+ return;
186
+ }
187
+
188
+ const cwd = process.cwd();
189
+
190
+ if (target === 'claude') {
191
+ this.uninstallClaude(cwd);
192
+ return;
193
+ }
194
+
195
+ const targetDir = getTargetDir(cwd, target);
196
+ if (await _exists(targetDir)) {
197
+ fs.rmSync(targetDir, { recursive: true, force: true });
198
+ printer.success(`Skills removed from ${path.relative(cwd, targetDir)}/`);
199
+ } else {
200
+ printer.warning(`No skills found at ${path.relative(cwd, targetDir)}/`);
201
+ }
202
+ }
203
+
204
+ uninstallClaude(cwd) {
205
+ const claudeFile = path.join(cwd, 'CLAUDE.md');
206
+ if (!fs.existsSync(claudeFile)) {
207
+ printer.warning('CLAUDE.md not found.');
208
+ return;
209
+ }
210
+ const content = fs.readFileSync(claudeFile, 'utf8');
211
+ const startMarker = `<!-- ${PKG_NAME}`;
212
+ const idx = content.indexOf(startMarker);
213
+ if (idx === -1) {
214
+ printer.warning('No @axiosleo/orm-mysql skills section found in CLAUDE.md.');
215
+ return;
216
+ }
217
+ const cleaned = content.substring(0, idx).trimEnd() + '\n';
218
+ fs.writeFileSync(claudeFile, cleaned);
219
+ printer.success('Skills section removed from CLAUDE.md');
220
+ }
221
+ }
222
+
223
+ module.exports = SkillsCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiosleo/orm-mysql",
3
- "version": "0.14.4",
3
+ "version": "0.15.0",
4
4
  "description": "MySQL ORM tool",
5
5
  "keywords": [
6
6
  "mysql",
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: orm-mysql-usage
3
+ description: Build MySQL queries, perform CRUD operations, and manage transactions using @axiosleo/orm-mysql. Use when writing database queries, building where conditions, inserting/updating/deleting rows, managing transactions, or working with the ORM query builder in this project.
4
+ ---
5
+
6
+ # @axiosleo/orm-mysql Usage Guide
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @axiosleo/orm-mysql
12
+ ```
13
+
14
+ ## Setup
15
+
16
+ ### Create a Connection
17
+
18
+ ```javascript
19
+ const { createClient, QueryHandler } = require("@axiosleo/orm-mysql");
20
+
21
+ const conn = createClient({
22
+ host: "localhost",
23
+ port: 3306,
24
+ user: "root",
25
+ password: "password",
26
+ database: "my_db",
27
+ });
28
+
29
+ const db = new QueryHandler(conn);
30
+ ```
31
+
32
+ ### Create a Connection Pool (recommended for production)
33
+
34
+ ```javascript
35
+ const { createPool, QueryHandler } = require("@axiosleo/orm-mysql");
36
+
37
+ const pool = createPool({
38
+ host: "localhost",
39
+ port: 3306,
40
+ user: "root",
41
+ password: "password",
42
+ database: "my_db",
43
+ connectionLimit: 10,
44
+ });
45
+
46
+ const db = new QueryHandler(pool);
47
+ ```
48
+
49
+ ### Using MySQLClient
50
+
51
+ ```javascript
52
+ const { MySQLClient } = require("@axiosleo/orm-mysql");
53
+
54
+ const client = new MySQLClient({
55
+ host: "localhost",
56
+ port: 3306,
57
+ user: "root",
58
+ password: "password",
59
+ database: "my_db",
60
+ }, null, "pool"); // "default" | "promise" | "pool"
61
+
62
+ const rows = await client.table("users").select();
63
+ await client.close();
64
+ ```
65
+
66
+ ## Class Hierarchy
67
+
68
+ ```
69
+ QueryCondition -- where clauses (where, whereIn, whereLike, whereBetween...)
70
+ └── Query -- query building (table, attr, join, orderBy, limit, page...)
71
+ └── QueryOperator -- execution (select, find, insert, update, delete...)
72
+ └── TransactionOperator -- adds append() for row locking
73
+ ```
74
+
75
+ - `QueryHandler` wraps a connection/pool and creates `QueryOperator` via `.table(name)`
76
+ - `TransactionHandler` wraps a promise connection and creates `TransactionOperator` via `.table(name)`
77
+
78
+ ## Quick Start
79
+
80
+ ```javascript
81
+ const db = new QueryHandler(conn);
82
+
83
+ // SELECT
84
+ const users = await db.table("users")
85
+ .where("age", ">", 18)
86
+ .orderBy("name", "asc")
87
+ .limit(10)
88
+ .select("id", "name", "age");
89
+
90
+ // INSERT
91
+ await db.table("users").insert({ name: "Joe", age: 25 });
92
+
93
+ // UPDATE
94
+ await db.table("users").where("id", 1).update({ age: 26 });
95
+
96
+ // DELETE
97
+ await db.table("users").where("id", 1).delete();
98
+
99
+ // COUNT
100
+ const total = await db.table("users").where("age", ">", 18).count();
101
+
102
+ // FIND single row
103
+ const user = await db.table("users").where("id", 1).find();
104
+ ```
105
+
106
+ ## Dry Run with notExec()
107
+
108
+ Call `notExec()` before any CRUD method to get a `Builder` object with `.sql` and `.values` instead of executing:
109
+
110
+ ```javascript
111
+ const builder = await db.table("users")
112
+ .where("age", ">", 18)
113
+ .notExec()
114
+ .select("id", "name");
115
+
116
+ console.log(builder.sql); // "SELECT `id`, `name` FROM `users` WHERE `age` > ?"
117
+ console.log(builder.values); // [18]
118
+ ```
119
+
120
+ ## Reference Files
121
+
122
+ | Scenario | File |
123
+ |----------|------|
124
+ | Building queries (table, join, orderBy, limit, groupBy, attr) | [query-building.md](query-building.md) |
125
+ | Where conditions (where, whereIn, whereLike, whereBetween...) | [where-conditions.md](where-conditions.md) |
126
+ | CRUD operations (select, find, count, insert, update, delete, incrBy, upsertRow) | [crud-operations.md](crud-operations.md) |
127
+ | Transactions (beginTransaction, commit, rollback, FOR UPDATE) | [transactions.md](transactions.md) |
128
+
129
+ ## Hooks
130
+
131
+ Register pre/post hooks for query operations:
132
+
133
+ ```javascript
134
+ const { Hook } = require("@axiosleo/orm-mysql");
135
+
136
+ Hook.pre(async (options) => {
137
+ console.log("Before:", options.operator, options.tables);
138
+ }, { table: "users", opt: "insert" });
139
+
140
+ Hook.post(async (options, result) => {
141
+ console.log("After:", result);
142
+ }, { table: "users", opt: "insert" });
143
+ ```
144
+
145
+ ## Schema Helpers
146
+
147
+ ```javascript
148
+ const exists = await db.existTable("users");
149
+ const dbExists = await db.existDatabase("my_db");
150
+ const fields = await db.getTableFields("my_db", "users", "COLUMN_NAME", "DATA_TYPE");
151
+ ```
152
+
153
+ ## Raw SQL
154
+
155
+ ```javascript
156
+ const result = await db.query({ sql: "SELECT * FROM users WHERE id = ?", values: [1] });
157
+ ```
@@ -0,0 +1,223 @@
1
+ # CRUD Operations
2
+
3
+ The `QueryOperator` class provides all execution methods. You get an instance via `db.table("name")`.
4
+
5
+ All CRUD methods return a `Promise`. Write operations resolve to `MySQLQueryResult` (OkPacket | ResultSetHeader). If `notExec()` was called, they resolve to a `Builder` object instead.
6
+
7
+ ## Read Operations
8
+
9
+ ### select(...attrs)
10
+
11
+ Returns an array of rows. Optionally pass column names to override previously set `attr()`.
12
+
13
+ ```javascript
14
+ const rows = await db.table("users").select();
15
+ const rows = await db.table("users").select("id", "name", "email");
16
+
17
+ // With TypeScript generics
18
+ interface User { id: number; name: string; email: string; }
19
+ const users = await db.table("users").select<User>("id", "name", "email");
20
+ ```
21
+
22
+ ### find()
23
+
24
+ Returns a single row (the first match). Automatically applies `LIMIT 1`.
25
+
26
+ ```javascript
27
+ const user = await db.table("users").where("id", 1).find();
28
+
29
+ // With TypeScript generics
30
+ const user = await db.table("users").where("id", 1).find<User>();
31
+ ```
32
+
33
+ ### count()
34
+
35
+ Returns the total number of matching rows as a number.
36
+
37
+ ```javascript
38
+ const total = await db.table("users").where("status", "active").count();
39
+ // total is a number
40
+ ```
41
+
42
+ ### explain(operator)
43
+
44
+ Returns the MySQL EXPLAIN result for the query.
45
+
46
+ ```javascript
47
+ const plan = await db.table("users")
48
+ .where("status", "active")
49
+ .explain("select");
50
+ // plan is ExplainResult[]
51
+ ```
52
+
53
+ ## Write Operations
54
+
55
+ ### insert(row)
56
+
57
+ Insert a single row.
58
+
59
+ ```javascript
60
+ const result = await db.table("users").insert({
61
+ name: "Joe",
62
+ age: 25,
63
+ email: "joe@example.com",
64
+ });
65
+ // result.insertId -- the auto-increment ID
66
+ // result.affectedRows -- number of rows inserted
67
+ ```
68
+
69
+ ### Insert with ON DUPLICATE KEY UPDATE
70
+
71
+ Use `keys()` to specify unique columns. If a duplicate is found, the operation becomes an update.
72
+
73
+ ```javascript
74
+ const result = await db.table("users").keys("email").insert({
75
+ email: "joe@example.com",
76
+ name: "Joe Updated",
77
+ age: 26,
78
+ });
79
+ ```
80
+
81
+ ### insertAll(rows)
82
+
83
+ Insert multiple rows. Returns an array of results.
84
+
85
+ ```javascript
86
+ const results = await db.table("users").insertAll([
87
+ { name: "Alice", age: 30 },
88
+ { name: "Bob", age: 28 },
89
+ { name: "Charlie", age: 35 },
90
+ ]);
91
+ // results is MySQLQueryResult[]
92
+ ```
93
+
94
+ ### update(row)
95
+
96
+ Update rows matching the current where conditions.
97
+
98
+ ```javascript
99
+ const result = await db.table("users")
100
+ .where("id", 1)
101
+ .update({ name: "Joe Updated", age: 26 });
102
+ // result.affectedRows -- number of rows updated
103
+ // result.changedRows -- number of rows actually changed
104
+ ```
105
+
106
+ ### delete(id?, index_field_name?)
107
+
108
+ Delete rows. Can be called with conditions or directly with an ID.
109
+
110
+ ```javascript
111
+ // Delete by conditions
112
+ const result = await db.table("users").where("status", "banned").delete();
113
+
114
+ // Delete by primary key (defaults to "id" field)
115
+ const result = await db.table("users").delete(1);
116
+
117
+ // Delete by a custom index field
118
+ const result = await db.table("users").delete(1, "user_id");
119
+ ```
120
+
121
+ ### incrBy(attr, increment?)
122
+
123
+ Increment a column value atomically. Default increment is 1.
124
+
125
+ ```javascript
126
+ // Increment by 1 (default)
127
+ await db.table("users").where("id", 1).incrBy("login_count");
128
+
129
+ // Increment by a specific number
130
+ await db.table("products").where("id", 1).incrBy("views", 5);
131
+
132
+ // Increment by string value
133
+ await db.table("users").where("id", 1).incrBy("score", "10");
134
+
135
+ // Conditional increment with callback
136
+ await db.table("users").where("id", 1).incrBy("error_count", (current) => {
137
+ return shouldIncrement ? 1 : 0;
138
+ });
139
+ ```
140
+
141
+ ### upsertRow(row, condition)
142
+
143
+ Insert a row or update it if a matching row exists based on the condition.
144
+
145
+ ```javascript
146
+ // Using QueryCondition
147
+ const condition = new QueryCondition();
148
+ condition.where("email", "joe@example.com");
149
+
150
+ await db.table("users").upsertRow(
151
+ { email: "joe@example.com", name: "Joe", age: 25 },
152
+ condition
153
+ );
154
+ ```
155
+
156
+ ## Dry Run with notExec()
157
+
158
+ Call `notExec()` before any CRUD method to get the generated SQL without executing it. Returns a `Builder` with `.sql` and `.values`.
159
+
160
+ ```javascript
161
+ const builder = await db.table("users")
162
+ .where("status", "active")
163
+ .orderBy("name", "asc")
164
+ .limit(10)
165
+ .notExec()
166
+ .select("id", "name");
167
+
168
+ console.log(builder.sql); // The generated SQL string
169
+ console.log(builder.values); // The parameter values array
170
+ ```
171
+
172
+ This works with all CRUD methods:
173
+
174
+ ```javascript
175
+ const insertBuilder = await db.table("users")
176
+ .notExec()
177
+ .insert({ name: "Joe", age: 25 });
178
+
179
+ const updateBuilder = await db.table("users")
180
+ .where("id", 1)
181
+ .notExec()
182
+ .update({ age: 26 });
183
+
184
+ const deleteBuilder = await db.table("users")
185
+ .where("id", 1)
186
+ .notExec()
187
+ .delete();
188
+ ```
189
+
190
+ ## Complete Example
191
+
192
+ ```javascript
193
+ const db = new QueryHandler(pool);
194
+
195
+ // Create user
196
+ const insertResult = await db.table("users").insert({
197
+ name: "Joe",
198
+ email: "joe@example.com",
199
+ age: 25,
200
+ });
201
+ const userId = insertResult.insertId;
202
+
203
+ // Read back
204
+ const user = await db.table("users").where("id", userId).find();
205
+
206
+ // Update
207
+ await db.table("users").where("id", userId).update({ age: 26 });
208
+
209
+ // Increment login count
210
+ await db.table("users").where("id", userId).incrBy("login_count");
211
+
212
+ // Count active users
213
+ const activeCount = await db.table("users").where("status", "active").count();
214
+
215
+ // Bulk insert
216
+ await db.table("logs").insertAll([
217
+ { user_id: userId, action: "login" },
218
+ { user_id: userId, action: "view_profile" },
219
+ ]);
220
+
221
+ // Delete
222
+ await db.table("sessions").where("expired_at", "<", new Date()).delete();
223
+ ```
@@ -0,0 +1,159 @@
1
+ # Query Building
2
+
3
+ The `Query` class provides the fluent API for constructing SQL queries. You get a `Query`-based instance (actually `QueryOperator`) from `QueryHandler.table()`.
4
+
5
+ ```javascript
6
+ const query = db.table("users"); // returns QueryOperator (extends Query)
7
+ ```
8
+
9
+ ## Table Selection
10
+
11
+ ### Single table
12
+
13
+ ```javascript
14
+ db.table("users");
15
+ db.table("users", "u"); // with alias
16
+ ```
17
+
18
+ ### Multiple tables
19
+
20
+ ```javascript
21
+ db.tables(
22
+ { table: "users", alias: "u" },
23
+ { table: "orders", alias: "o" }
24
+ );
25
+ ```
26
+
27
+ ## Selecting Columns with attr()
28
+
29
+ ```javascript
30
+ // Select specific columns
31
+ query.attr("id", "name", "email");
32
+
33
+ // Using sub-query as attribute
34
+ const { Query } = require("@axiosleo/orm-mysql");
35
+
36
+ query.attr(
37
+ "id",
38
+ "name",
39
+ () => {
40
+ const sub = new Query("select");
41
+ sub.table("orders").attr("COUNT(*)").where("orders.user_id", "users.id");
42
+ return sub;
43
+ }
44
+ );
45
+ ```
46
+
47
+ Calling `attr()` with no arguments clears all previously set attributes.
48
+
49
+ ## Pagination
50
+
51
+ ### limit and offset
52
+
53
+ ```javascript
54
+ query.limit(10); // LIMIT 10
55
+ query.offset(20); // OFFSET 20
56
+ ```
57
+
58
+ ### page (shorthand)
59
+
60
+ ```javascript
61
+ query.page(10); // LIMIT 10 OFFSET 0
62
+ query.page(10, 2); // LIMIT 10 OFFSET 2
63
+ ```
64
+
65
+ ## Sorting
66
+
67
+ ```javascript
68
+ query.orderBy("created_at", "desc");
69
+ query.orderBy("name", "asc");
70
+ // Multiple orderBy calls are cumulative
71
+ ```
72
+
73
+ ## Grouping and Aggregation
74
+
75
+ ```javascript
76
+ query.groupBy("status");
77
+ query.groupBy("status", "category"); // multiple fields
78
+
79
+ // HAVING clause (requires groupBy)
80
+ query.groupBy("status").having("COUNT(*)", ">", 5);
81
+ ```
82
+
83
+ ## Setting Data
84
+
85
+ ### set() -- for insert/update data
86
+
87
+ ```javascript
88
+ query.set({ name: "Joe", age: 25 });
89
+ ```
90
+
91
+ ### keys() -- specify columns for INSERT with ON DUPLICATE KEY UPDATE
92
+
93
+ ```javascript
94
+ query.keys("uuid").insert({
95
+ uuid: "abc-123",
96
+ name: "Joe",
97
+ age: 25,
98
+ });
99
+ // If uuid already exists, the insert becomes an update
100
+ ```
101
+
102
+ ## Joins
103
+
104
+ ### leftJoin / rightJoin / innerJoin (preferred)
105
+
106
+ ```javascript
107
+ query.table("users", "u")
108
+ .leftJoin("orders", "u.id = orders.user_id", { alias: "o" })
109
+ .attr("u.id", "u.name", "o.total")
110
+ .select();
111
+
112
+ query.table("users", "u")
113
+ .rightJoin("orders", "u.id = orders.user_id")
114
+ .select();
115
+
116
+ query.table("users", "u")
117
+ .innerJoin("roles", "u.role_id = roles.id", { alias: "r" })
118
+ .select();
119
+ ```
120
+
121
+ ### join() -- generic form
122
+
123
+ ```javascript
124
+ query.join({
125
+ table: "orders",
126
+ table_alias: "o",
127
+ self_column: "id",
128
+ foreign_column: "user_id",
129
+ join_type: "left",
130
+ });
131
+ ```
132
+
133
+ ### Sub-query as join table
134
+
135
+ ```javascript
136
+ const { Query } = require("@axiosleo/orm-mysql");
137
+
138
+ const subQuery = new Query("select");
139
+ subQuery.table("orders").attr("user_id", "SUM(total) AS order_total").groupBy("user_id");
140
+
141
+ query.table("users", "u")
142
+ .leftJoin(subQuery, "u.id = sub.user_id", { alias: "sub" })
143
+ .select();
144
+ ```
145
+
146
+ ## Complete Example
147
+
148
+ ```javascript
149
+ const users = await db.table("users", "u")
150
+ .attr("u.id", "u.name", "u.email", "o.total")
151
+ .leftJoin("orders", "u.id = orders.user_id", { alias: "o" })
152
+ .where("u.status", "active")
153
+ .whereBetween("u.created_at", ["2024-01-01", "2024-12-31"])
154
+ .groupBy("u.id")
155
+ .having("COUNT(o.id)", ">", 0)
156
+ .orderBy("u.name", "asc")
157
+ .page(20, 0)
158
+ .select();
159
+ ```
@@ -0,0 +1,171 @@
1
+ # Transactions
2
+
3
+ ## Isolation Levels
4
+
5
+ | Shorthand | Full Name | Description |
6
+ |-----------|-----------|-------------|
7
+ | `RU` | `READ UNCOMMITTED` | Lowest isolation, may read dirty data |
8
+ | `RC` | `READ COMMITTED` | Prevents dirty reads |
9
+ | `RR` | `REPEATABLE READ` | MySQL default, prevents non-repeatable reads |
10
+ | `S` | `SERIALIZABLE` | Highest isolation, full serialization |
11
+
12
+ ## Method 1: Pool + beginTransaction (Recommended)
13
+
14
+ Use `QueryHandler.beginTransaction()` with a connection pool. The transaction automatically gets its own connection from the pool and releases it on commit/rollback.
15
+
16
+ ```javascript
17
+ const { createPool, QueryHandler } = require("@axiosleo/orm-mysql");
18
+
19
+ const pool = createPool({
20
+ host: "localhost",
21
+ port: 3306,
22
+ user: "root",
23
+ password: "password",
24
+ database: "my_db",
25
+ connectionLimit: 10,
26
+ });
27
+
28
+ const db = new QueryHandler(pool);
29
+
30
+ const tx = await db.beginTransaction({ level: "RC" });
31
+ try {
32
+ const result = await tx.table("users").insert({ name: "Joe", age: 25 });
33
+ const userId = result.insertId;
34
+
35
+ await tx.table("profiles").insert({ user_id: userId, bio: "Hello" });
36
+
37
+ await tx.commit();
38
+ } catch (err) {
39
+ await tx.rollback();
40
+ throw err;
41
+ }
42
+ ```
43
+
44
+ ## Method 2: TransactionHandler Directly
45
+
46
+ For more control, create a `TransactionHandler` with a promise connection.
47
+
48
+ ```javascript
49
+ const { TransactionHandler, createPromiseClient } = require("@axiosleo/orm-mysql");
50
+
51
+ const conn = await createPromiseClient({
52
+ host: "localhost",
53
+ port: 3306,
54
+ user: "root",
55
+ password: "password",
56
+ database: "my_db",
57
+ });
58
+
59
+ const tx = new TransactionHandler(conn, { level: "SERIALIZABLE" });
60
+ await tx.begin();
61
+
62
+ try {
63
+ await tx.table("users").insert({ name: "Joe", age: 25 });
64
+ await tx.commit();
65
+ } catch (err) {
66
+ await tx.rollback();
67
+ throw err;
68
+ }
69
+ ```
70
+
71
+ ## Row Locking
72
+
73
+ `TransactionOperator` (returned by `tx.table()`) extends `QueryOperator` with `append()` for SQL suffixes.
74
+
75
+ ### FOR UPDATE
76
+
77
+ Locks selected rows, preventing other transactions from reading or modifying them.
78
+
79
+ ```javascript
80
+ const tx = await db.beginTransaction({ level: "RC" });
81
+ try {
82
+ const product = await tx.table("products")
83
+ .where("sku", "LAPTOP-001")
84
+ .append("FOR UPDATE")
85
+ .find();
86
+
87
+ if (product.stock < 1) {
88
+ throw new Error("Out of stock");
89
+ }
90
+
91
+ await tx.table("products")
92
+ .where("sku", "LAPTOP-001")
93
+ .update({ stock: product.stock - 1 });
94
+
95
+ await tx.table("orders").insert({
96
+ product_id: product.id,
97
+ quantity: 1,
98
+ total: product.price,
99
+ });
100
+
101
+ await tx.commit();
102
+ } catch (err) {
103
+ await tx.rollback();
104
+ throw err;
105
+ }
106
+ ```
107
+
108
+ ### LOCK IN SHARE MODE
109
+
110
+ Allows other transactions to read but not modify the locked rows.
111
+
112
+ ```javascript
113
+ const rows = await tx.table("products")
114
+ .where("category", "electronics")
115
+ .append("LOCK IN SHARE MODE")
116
+ .select();
117
+ ```
118
+
119
+ ## TransactionHandler API
120
+
121
+ | Method | Description |
122
+ |--------|-------------|
123
+ | `begin()` | Start the transaction |
124
+ | `commit()` | Commit and release the connection |
125
+ | `rollback()` | Rollback and release the connection |
126
+ | `table(name, alias?)` | Returns `TransactionOperator` for the table |
127
+ | `query(options)` | Execute raw SQL within the transaction |
128
+ | `execute(sql, values)` | Execute parameterized SQL |
129
+ | `lastInsertId(alias?)` | Get the last auto-increment ID |
130
+ | `upsert(table, data, condition)` | Insert or update within the transaction |
131
+
132
+ ## Concurrent Transactions
133
+
134
+ With a connection pool, multiple transactions run on separate connections without blocking each other.
135
+
136
+ ```javascript
137
+ const db = new QueryHandler(pool);
138
+
139
+ await Promise.all([
140
+ (async () => {
141
+ const tx = await db.beginTransaction();
142
+ try {
143
+ await tx.table("users").insert({ name: "User1" });
144
+ await tx.commit();
145
+ } catch (err) {
146
+ await tx.rollback();
147
+ throw err;
148
+ }
149
+ })(),
150
+
151
+ (async () => {
152
+ const tx = await db.beginTransaction();
153
+ try {
154
+ await tx.table("users").insert({ name: "User2" });
155
+ await tx.commit();
156
+ } catch (err) {
157
+ await tx.rollback();
158
+ throw err;
159
+ }
160
+ })(),
161
+ ]);
162
+ ```
163
+
164
+ ## Best Practices
165
+
166
+ 1. **Use connection pools in production** -- prevents connection exhaustion
167
+ 2. **Always wrap in try/catch** -- ensure rollback on errors
168
+ 3. **Keep transactions short** -- avoid long-running operations inside transactions
169
+ 4. **Choose the right isolation level** -- `RC` is a good default for most cases
170
+ 5. **Use row locking when needed** -- `FOR UPDATE` prevents concurrent modification conflicts
171
+ 6. **Prefer `beginTransaction()` over manual `TransactionHandler`** -- automatic connection management
@@ -0,0 +1,146 @@
1
+ # Where Conditions
2
+
3
+ The `QueryCondition` class provides all WHERE clause methods. All methods return `this` for chaining.
4
+
5
+ ## Basic where()
6
+
7
+ ### Key-value equality
8
+
9
+ ```javascript
10
+ query.where("name", "Joe"); // WHERE `name` = ?
11
+ query.where("age", ">", 18); // WHERE `age` > ?
12
+ query.where("status", "!=", "banned"); // WHERE `status` != ?
13
+ ```
14
+
15
+ ### Object form (multiple equalities)
16
+
17
+ ```javascript
18
+ query.where({ name: "Joe", status: "active" });
19
+ // WHERE `name` = ? AND `status` = ?
20
+ ```
21
+
22
+ ### Supported operators
23
+
24
+ `=`, `!=`, `>`, `<`, `>=`, `<=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `BETWEEN`, `NOT BETWEEN`, `IS`, `IS NOT`, `REGEXP`, `NOT REGEXP`, `CONTAIN`, `NOT CONTAIN`, `OVERLAPS`, `NOT OVERLAPS`
25
+
26
+ ## Logical Operators
27
+
28
+ ### AND / OR grouping
29
+
30
+ ```javascript
31
+ // Switch to OR logic for subsequent conditions
32
+ query.where("OR");
33
+ // or equivalently:
34
+ query.whereOr();
35
+
36
+ // Switch back to AND logic
37
+ query.where("AND");
38
+ // or equivalently:
39
+ query.whereAnd();
40
+ ```
41
+
42
+ ### Combining AND/OR
43
+
44
+ ```javascript
45
+ // WHERE `status` = ? AND (`age` > ? OR `vip` = ?)
46
+ query
47
+ .where("status", "active")
48
+ .whereOr()
49
+ .where("age", ">", 18)
50
+ .where("vip", true)
51
+ .whereAnd();
52
+ ```
53
+
54
+ ## IN / NOT IN
55
+
56
+ ```javascript
57
+ query.whereIn("status", ["active", "pending"]);
58
+ // WHERE `status` IN (?, ?)
59
+
60
+ query.whereNotIn("role", ["banned", "suspended"]);
61
+ // WHERE `role` NOT IN (?, ?)
62
+ ```
63
+
64
+ ### Sub-query in whereIn
65
+
66
+ ```javascript
67
+ const { Query } = require("@axiosleo/orm-mysql");
68
+
69
+ const subQuery = new Query("select");
70
+ subQuery.table("orders").attr("user_id").where("total", ">", 100);
71
+
72
+ query.whereIn("id", subQuery);
73
+ // WHERE `id` IN (SELECT `user_id` FROM `orders` WHERE `total` > ?)
74
+ ```
75
+
76
+ ## LIKE / NOT LIKE
77
+
78
+ ```javascript
79
+ query.whereLike("name", "%Joe%");
80
+ // WHERE `name` LIKE ?
81
+
82
+ query.whereNotLike("email", "%spam%");
83
+
84
+ // Multiple patterns (OR)
85
+ query.whereLike("name", ["%Joe%", "%Jane%"]);
86
+ ```
87
+
88
+ ## BETWEEN / NOT BETWEEN
89
+
90
+ ```javascript
91
+ query.whereBetween("age", [18, 65]);
92
+ // WHERE `age` BETWEEN ? AND ?
93
+
94
+ query.whereNotBetween("created_at", ["2024-01-01", "2024-06-30"]);
95
+ ```
96
+
97
+ ## CONTAIN / NOT CONTAIN
98
+
99
+ For JSON array or SET column checks:
100
+
101
+ ```javascript
102
+ query.whereContain("tags", "javascript");
103
+ // WHERE JSON_CONTAINS(`tags`, ?)
104
+
105
+ query.whereNotContain("tags", "deprecated");
106
+ ```
107
+
108
+ ## OVERLAPS / NOT OVERLAPS
109
+
110
+ For JSON array overlap checks:
111
+
112
+ ```javascript
113
+ query.whereOverlaps("categories", [1, 2, 3]);
114
+
115
+ query.whereNotOverlaps("categories", [4, 5]);
116
+ ```
117
+
118
+ ## Nested Conditions with whereCondition()
119
+
120
+ Use `QueryCondition` to build complex nested conditions:
121
+
122
+ ```javascript
123
+ const { QueryCondition } = require("@axiosleo/orm-mysql");
124
+
125
+ const nested = new QueryCondition();
126
+ nested.where("age", ">", 18).where("age", "<", 65);
127
+
128
+ query
129
+ .where("status", "active")
130
+ .whereCondition(nested);
131
+ // WHERE `status` = ? AND (`age` > ? AND `age` < ?)
132
+ ```
133
+
134
+ ## Complete Example
135
+
136
+ ```javascript
137
+ const results = await db.table("products", "p")
138
+ .where("p.status", "active")
139
+ .whereBetween("p.price", [10, 100])
140
+ .whereIn("p.category_id", [1, 2, 3])
141
+ .whereLike("p.name", "%phone%")
142
+ .whereNotIn("p.id", blockedIds)
143
+ .orderBy("p.price", "asc")
144
+ .page(20, 0)
145
+ .select();
146
+ ```