@mlagie/sql-connector 2.0.4 → 2.0.6
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/.github/workflows/publish.yml +16 -0
- package/package.json +4 -3
- package/releases/2.0.5.md +65 -0
- package/releases/2.0.6.md +80 -0
- package/src/models/Model.js +3 -18
- package/src/models/ModelInstance.js +19 -28
- package/src/utils/buildQuery.js +5 -1
- package/src/utils/formatObject.js +1 -0
- package/src/utils/generateCondition.js +0 -8
- package/tests/Model.test.js +666 -0
- package/tests/ModelInstance.test.js +335 -0
- package/tests/buildQuery.test.js +89 -1
- package/tests/connect.test.js +14 -4
- package/tests/formatObject.test.js +9 -0
- package/tests/generateCondition.test.js +200 -0
|
@@ -44,10 +44,26 @@ jobs:
|
|
|
44
44
|
continue-on-error: false
|
|
45
45
|
run: |
|
|
46
46
|
npm run test
|
|
47
|
+
check_publish:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- name: Checkout GH repository
|
|
51
|
+
uses: actions/checkout@v6
|
|
52
|
+
- name: setup node
|
|
53
|
+
uses: actions/setup-node@v6
|
|
54
|
+
with:
|
|
55
|
+
node-version: 22
|
|
56
|
+
- name: Install deps
|
|
57
|
+
run: npm ci
|
|
58
|
+
- name: Check if package can be published
|
|
59
|
+
continue-on-error: false
|
|
60
|
+
run: |
|
|
61
|
+
npm run test:publish
|
|
47
62
|
publish:
|
|
48
63
|
needs:
|
|
49
64
|
- check_security
|
|
50
65
|
- integrity
|
|
66
|
+
- check_publish
|
|
51
67
|
if: github.event_name == 'release'
|
|
52
68
|
runs-on: ubuntu-latest
|
|
53
69
|
environment: sqlc
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "Le module sql-connector permet de gérer les connexions à une base de données MySQL, de définir des schémas de tables, et d'interagir avec les données de manière simple et efficace.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
|
|
7
|
+
"test": "rm -r coverage ; node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
|
|
8
|
+
"test:publish": "npm publish --dry-run",
|
|
8
9
|
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
9
|
-
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
10
|
+
"test:coverage": "rm -r coverage ; node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
10
11
|
"security": "npx eslint . --max-warnings 0 --ignore-pattern './tests/*'"
|
|
11
12
|
},
|
|
12
13
|
"repository": {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Release v2.0.5 — Aggregation Engine Expansion (COUNT, DISTINCT & Group Matrix Resolution)
|
|
2
|
+
|
|
3
|
+
This release expands the dynamic query builder capability to support structured multi-row statistical analysis (`COUNT` and `DISTINCT`) while maintaining full architectural alignment with our strict SQL Safety parameters.
|
|
4
|
+
|
|
5
|
+
## What Was Improved
|
|
6
|
+
|
|
7
|
+
- **Native Secure** `COUNT` `Aggregation Matrix`: Introduced the structured `{ count: "*" }` or `{ count: "column_name" }` block into `utils/buildQuery.js`. This allows developers to safely generate standard SQL COUNT() aggregations without raw string shortcuts triggering safety breaches in escapeIdentifier().
|
|
8
|
+
|
|
9
|
+
- **Distinct Value Interception Support**: Optimized fields evaluation layout to support `DISTINCT` processing inside column mappings and `GROUP BY` structural nodes, resolving compatibility limits under aggressive `ONLY_FULL_GROUP_BY` database runtime settings.
|
|
10
|
+
|
|
11
|
+
- `Granular Regression Controls`: Hardened testing architecture with direct atomic assertions on nested selection objects to ensure utility stability over complex ingestion flows.
|
|
12
|
+
|
|
13
|
+
## **Unit Test Additions** (`tests/buildQuery.test.js`)
|
|
14
|
+
|
|
15
|
+
To ensure functional continuity, the selection building block test matrix has been expanded with the following scenarios:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
describe('buildQuery - Advanced Fields & Aggregations', () => {
|
|
19
|
+
|
|
20
|
+
test('Should safely compile COUNT(*) aggregated calculations with an expression alias', () => {
|
|
21
|
+
const fields = [{ count: '*', as: 'total_user' }];
|
|
22
|
+
expect(buildSelect(fields)).toBe('COUNT(*) AS `total_user`');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('Should safely compile COUNT(column) on structured column identifiers', () => {
|
|
26
|
+
const fields = [{ count: 'id', as: 'unique_ids' }];
|
|
27
|
+
expect(buildSelect(fields)).toBe('COUNT(`id`) AS `unique_ids`');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('Should correctly process multi-column GROUP BY arrays to avoid ONLY_FULL_GROUP_BY validation issues', () => {
|
|
31
|
+
const options = {
|
|
32
|
+
select: ['status', 'email', { count: '*', as: 'total_user' }],
|
|
33
|
+
groupBy: ['status', 'email']
|
|
34
|
+
};
|
|
35
|
+
const parts = buildQueryParts(options);
|
|
36
|
+
|
|
37
|
+
expect(parts).toContain('GROUP BY `status`, `email`');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Component Impact
|
|
43
|
+
|
|
44
|
+
| Impacted Area | Description | Status |
|
|
45
|
+
|------------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------|
|
|
46
|
+
| **buildQuery.js** | Added `field.count` mapping to isolate raw expressions (`*`) safely from structural identifiers formatting. | **Upgraded** |
|
|
47
|
+
| **tests/buildQuery.test.js** | Integrated targeted coverage checking for isolated function behaviors and multi-field groups. | **Covered** |
|
|
48
|
+
| **Data Ingestion Modules** | Safely resolving analytical aggregations (like cross-team platform data pipeline counters) in single-trip transactions. | **Resolved** |
|
|
49
|
+
|
|
50
|
+
## Usage Example (Analytics Ingestion)
|
|
51
|
+
|
|
52
|
+
Instead of passing unstable raw operations, leverage the new token syntax in your models:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
const teamNameData = await ProjectPipeline.find({
|
|
56
|
+
select: [
|
|
57
|
+
"team",
|
|
58
|
+
"source",
|
|
59
|
+
{ count: "*", as: "total_pipelines" } // Raw counting handled securely
|
|
60
|
+
],
|
|
61
|
+
groupBy: ["team", "source"] // Aligned with database safety policies
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
*This release is fully covered by our automated CI/CD pipeline verification gates. Run npm update `@mlagie/sql-connector` to align your staging environments.*
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Release v2.0.6 — Architectural Property Isolation, Query Defusal & Test Coverage Expansion
|
|
2
|
+
|
|
3
|
+
This emergency structural release addresses a critical data-shadowing bug within the `ModelInstance` hydration cycle where active database column values (e.g., fields named `name`, `schema`, or `data`) overrode the core internal engine structures, mangling runtime query generation. It also introduces a comprehensive overhaul of the test suite to secure maximum coverage.
|
|
4
|
+
|
|
5
|
+
## 🛠️ The Bug Defusal (Why this was critical)
|
|
6
|
+
|
|
7
|
+
When `ModelInstance` mapped rows dynamically into getters/setters, a column named `name` from the database would mask `this.name` (the structural reference to the SQL Table Name).
|
|
8
|
+
|
|
9
|
+
As a result, an injection or ingestion update would mistakenly compile syntax failures like:
|
|
10
|
+
|
|
11
|
+
```sql
|
|
12
|
+
UPDATE my_value SET `deletedAt` = ... -- Mangled: Used row data instead of Table Name
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
By migrating all runtime configuration tokens to protected underscored properties (`_tableName`, `_data`, `_schema`), the instance context is now completely hermetic against database row schema payloads.
|
|
16
|
+
|
|
17
|
+
## Massive Test Coverage Expansion
|
|
18
|
+
|
|
19
|
+
To ensure the stability and robustness of this release, the unit test suites for both `Model` and `ModelInstance` have been significantly hardened:
|
|
20
|
+
|
|
21
|
+
- **Edge-Case & Fallback Testing**: Added specialized tests to force controlled failures in deep `try/catch` blocks, guaranteeing that the global engine fallback workflows function flawlessly under corrupted data scenarios.
|
|
22
|
+
|
|
23
|
+
- **SQL Mechanics Validation**: Built targeted unit tests covering schema dictionary mismatches, complex conditional mapping (e.g., table joins filtering mixed array types), and secondary logic paths inside `updateOne()`, `delete()`, and `deleteOne()`.
|
|
24
|
+
|
|
25
|
+
- `Database Simulation Coverage`: Implemented strict simulation sequences verifying UUID collision rejections (ensuring `generate_uuid()` handles duplicates properly) and missing response payloads (such as undefined `affectedRows`).
|
|
26
|
+
|
|
27
|
+
- `Zero-Crash Guarantees`: Hardened utility methods like `getFieldType()` against historical JavaScript edge cases (such as parsing `null` values safely).
|
|
28
|
+
|
|
29
|
+
## What Was Improved
|
|
30
|
+
|
|
31
|
+
- `Protected Context Isolation`: Migrated core metadata targets in `ModelInstance.js` to `_tableName`, `_data`, and `_schema`. Database columns can no longer shadow engine properties.
|
|
32
|
+
|
|
33
|
+
- `Tiret/Hyphen Database Safety`: Wrapped table calls explicitly in SQL backticks (\`) inside `updateOne`, `delete`, and `deleteOne` routines. Tables named with hyphens (like my_value) no longer trigger syntax violations.
|
|
34
|
+
|
|
35
|
+
- **Rehydration Mapping Consistency**: Standardized downstream sub-instantiations (like `customRequest`) to feed data strictly using the isolated `_tableName` descriptor.
|
|
36
|
+
|
|
37
|
+
## File Blueprint Updates
|
|
38
|
+
|
|
39
|
+
`ModelInstance.js`
|
|
40
|
+
|
|
41
|
+
The updated architecture encapsulates the internal core parameters like this:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
class ModelInstance {
|
|
45
|
+
constructor(name, data, schema = null) {
|
|
46
|
+
Object.defineProperties(this, {
|
|
47
|
+
_tableName: { // 🔒 Fully protected from row data collisions
|
|
48
|
+
value: name,
|
|
49
|
+
writable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
enumerable: false
|
|
52
|
+
},
|
|
53
|
+
_data: {
|
|
54
|
+
value: data,
|
|
55
|
+
writable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
enumerable: true
|
|
58
|
+
},
|
|
59
|
+
_schema: {
|
|
60
|
+
value: schema,
|
|
61
|
+
writable: true,
|
|
62
|
+
configurable: true,
|
|
63
|
+
enumerable: false
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Dynamic row mapping safely bound without property leakage...
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async updateOne(model) {
|
|
71
|
+
const setClause = generateCondition(formatObject(model), true);
|
|
72
|
+
let whereClause = /* ... evaluation matrix using this._schema & this.getRecordData() ... */;
|
|
73
|
+
|
|
74
|
+
// Encapsulated Table execution with explicit backticks
|
|
75
|
+
const sql_request = `UPDATE \`${this._tableName}\` SET ${setClause} WHERE ${whereClause}`;
|
|
76
|
+
|
|
77
|
+
// ... execute transaction safely
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
package/src/models/Model.js
CHANGED
|
@@ -5,7 +5,6 @@ const generateCondition = require("../utils/generateCondition");
|
|
|
5
5
|
const formatObject = require("../utils/formatObject");
|
|
6
6
|
const { ModelInstance } = require("./ModelInstance");
|
|
7
7
|
const { buildSelect, buildQueryParts } = require("../utils/buildQuery");
|
|
8
|
-
const util = require("util");
|
|
9
8
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
10
9
|
const { escapeIdentifier, escapeIdentifierList } = require("../utils/sql");
|
|
11
10
|
|
|
@@ -14,10 +13,8 @@ function getFieldType(field) {
|
|
|
14
13
|
if (field.type && field.type.name !== undefined) return field.type.name;
|
|
15
14
|
else if (field.type !== undefined) return field.type;
|
|
16
15
|
return undefined;
|
|
17
|
-
} else {
|
|
18
|
-
if (field && field.name !== undefined) return field.name;
|
|
19
|
-
else return field;
|
|
20
16
|
}
|
|
17
|
+
if (field && field.name !== undefined) return field.name;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
function isDateLikeType(fieldType) {
|
|
@@ -99,7 +96,7 @@ function getColumnDefinition(fieldName, field) {
|
|
|
99
96
|
if (defaultDefinition !== null) colDef += ` ${defaultDefinition}`;
|
|
100
97
|
if (field.unique) colDef += ' UNIQUE';
|
|
101
98
|
if (field.auto_increment) colDef += ' AUTO_INCREMENT';
|
|
102
|
-
if (field.primary_key) colDef += ' PRIMARY KEY'
|
|
99
|
+
if (field.primary_key) colDef += ' PRIMARY KEY'
|
|
103
100
|
if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
|
|
104
101
|
return `${escapeIdentifier(fieldName)} ${colDef}`;
|
|
105
102
|
}
|
|
@@ -213,18 +210,6 @@ class Model {
|
|
|
213
210
|
return `CREATE TABLE IF NOT EXISTS ${escapeIdentifier(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ENGINE=InnoDB`;
|
|
214
211
|
}
|
|
215
212
|
|
|
216
|
-
getRecordData() {
|
|
217
|
-
return Array.isArray(this.data) ? this.data[0] ?? this.data : this.data;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
toJSON() {
|
|
221
|
-
return this.getRecordData()?.data;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
[util.inspect.custom]() {
|
|
225
|
-
return this.getRecordData();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
213
|
/**
|
|
229
214
|
* Saves data to the database table.
|
|
230
215
|
* @param {Object} data The data to insert into the table.
|
|
@@ -346,7 +331,7 @@ class Model {
|
|
|
346
331
|
return resolve(1);
|
|
347
332
|
}).catch((err) => {
|
|
348
333
|
error(`Error executing query delete: ${err}`);
|
|
349
|
-
|
|
334
|
+
reject(err)
|
|
350
335
|
});
|
|
351
336
|
});
|
|
352
337
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
const { error } = require("@mlagie/logger");
|
|
1
|
+
const { error, logs } = require("@mlagie/logger");
|
|
2
2
|
const { getConnexion } = require("../db/connexion");
|
|
3
3
|
const formatObject = require("../utils/formatObject");
|
|
4
4
|
const generateCondition = require("../utils/generateCondition");
|
|
5
|
-
const util = require("util");
|
|
6
5
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -18,19 +17,19 @@ class ModelInstance {
|
|
|
18
17
|
*/
|
|
19
18
|
constructor(name, data, schema = null) {
|
|
20
19
|
Object.defineProperties(this, {
|
|
21
|
-
|
|
20
|
+
_name: {
|
|
22
21
|
value: name,
|
|
23
22
|
writable: true,
|
|
24
23
|
configurable: true,
|
|
25
24
|
enumerable: false
|
|
26
25
|
},
|
|
27
|
-
|
|
26
|
+
_data: {
|
|
28
27
|
value: data,
|
|
29
28
|
writable: true,
|
|
30
29
|
configurable: true,
|
|
31
30
|
enumerable: true
|
|
32
31
|
},
|
|
33
|
-
|
|
32
|
+
_schema: {
|
|
34
33
|
value: schema,
|
|
35
34
|
writable: true,
|
|
36
35
|
configurable: true,
|
|
@@ -63,22 +62,18 @@ class ModelInstance {
|
|
|
63
62
|
* @private
|
|
64
63
|
*/
|
|
65
64
|
_getTargetRow() {
|
|
66
|
-
const rows = Array.isArray(this.
|
|
65
|
+
const rows = Array.isArray(this._data) && Array.isArray(this._data[0]) ? this._data[0] : this._data;
|
|
67
66
|
return Array.isArray(rows) ? rows[0] : rows;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
getRecordData() {
|
|
71
|
-
return Array.isArray(this.
|
|
70
|
+
return Array.isArray(this._data) ? this._data[0] ?? this._data : this._data;
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
toJSON() {
|
|
75
74
|
return this.getRecordData();
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
[util.inspect.custom]() {
|
|
79
|
-
return this.getRecordData();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
77
|
/**
|
|
83
78
|
* Updates a single entry in the database table.
|
|
84
79
|
*
|
|
@@ -96,15 +91,11 @@ class ModelInstance {
|
|
|
96
91
|
let rawRec = Array.isArray(recordsArray) ? recordsArray[0] : recordsArray;
|
|
97
92
|
|
|
98
93
|
if (typeof rawRec === 'string') {
|
|
99
|
-
|
|
100
|
-
rawRec = JSON.parse(rawRec);
|
|
101
|
-
} catch {
|
|
102
|
-
rawRec = recordsArray;
|
|
103
|
-
}
|
|
94
|
+
rawRec = JSON.parse(rawRec);
|
|
104
95
|
}
|
|
105
96
|
const rec = rawRec;
|
|
106
97
|
|
|
107
|
-
const schemaDict = this.
|
|
98
|
+
const schemaDict = this._schema && this._schema.schemaDict ? this._schema.schemaDict : null;
|
|
108
99
|
if (schemaDict) {
|
|
109
100
|
const pkKeys = Object.entries(schemaDict).filter(([, v]) => v && v.primary_key === true).map(([k]) => k);
|
|
110
101
|
if (pkKeys.length > 0) {
|
|
@@ -112,20 +103,19 @@ class ModelInstance {
|
|
|
112
103
|
for (const k of pkKeys) {
|
|
113
104
|
if (rec && Object.prototype.hasOwnProperty.call(rec, k)) setSafe(pkObj, k, getSafe(rec, k));
|
|
114
105
|
}
|
|
115
|
-
if (Object.keys(pkObj).length > 0) whereClause = generateCondition(formatObject(pkObj), false, this.
|
|
106
|
+
if (Object.keys(pkObj).length > 0) whereClause = generateCondition(formatObject(pkObj), false, this._schema);
|
|
116
107
|
}
|
|
117
108
|
}
|
|
118
|
-
if (!whereClause) whereClause = generateCondition(formatObject(rec), false, this.
|
|
109
|
+
if (!whereClause) whereClause = generateCondition(formatObject(rec), false, this._schema);
|
|
119
110
|
} catch {
|
|
120
111
|
const originalFallbackRec = this.getRecordData();
|
|
121
112
|
let fallbackRec = originalFallbackRec;
|
|
122
113
|
if (Array.isArray(fallbackRec)) fallbackRec = fallbackRec[0];
|
|
123
114
|
if (typeof fallbackRec === 'string') { try { fallbackRec = JSON.parse(fallbackRec); } catch { fallbackRec = originalFallbackRec; } }
|
|
124
|
-
whereClause = generateCondition(formatObject(fallbackRec), false, this.
|
|
115
|
+
whereClause = generateCondition(formatObject(fallbackRec), false, this._schema);
|
|
125
116
|
}
|
|
126
117
|
|
|
127
|
-
const sql_request = `UPDATE ${this.
|
|
128
|
-
|
|
118
|
+
const sql_request = `UPDATE ${this._name} SET ${setClause} WHERE ${whereClause}`;
|
|
129
119
|
const [result] = await getConnexion().promise().execute(sql_request).catch((err) => {
|
|
130
120
|
error(`Error executing query updateOne: ${err}`);
|
|
131
121
|
throw err;
|
|
@@ -135,10 +125,10 @@ class ModelInstance {
|
|
|
135
125
|
|
|
136
126
|
if (affected > 0) {
|
|
137
127
|
const record = this.getRecordData();
|
|
138
|
-
if (Array.isArray(this.
|
|
139
|
-
if (this.
|
|
128
|
+
if (Array.isArray(this._data)) {
|
|
129
|
+
if (this._data[0] && typeof this._data[0] === 'object') Object.assign(this._data[0], model);
|
|
140
130
|
} else if (record && typeof record === 'object') {
|
|
141
|
-
Object.assign(this.
|
|
131
|
+
Object.assign(this._data, model);
|
|
142
132
|
}
|
|
143
133
|
}
|
|
144
134
|
|
|
@@ -152,7 +142,7 @@ class ModelInstance {
|
|
|
152
142
|
* @throws {Error} Throws an error if the deletion fails.
|
|
153
143
|
*/
|
|
154
144
|
async delete(filter) {
|
|
155
|
-
const sql_request = `DELETE FROM ${this.
|
|
145
|
+
const sql_request = `DELETE FROM ${this._name} WHERE ${generateCondition(formatObject(filter))}`;
|
|
156
146
|
|
|
157
147
|
const rows = await getConnexion().promise().execute(sql_request).catch((err) => {
|
|
158
148
|
error(`Error executing query delete: ${err}`);
|
|
@@ -168,8 +158,9 @@ class ModelInstance {
|
|
|
168
158
|
* @throws {Error} Throws an error if the deletion fails.
|
|
169
159
|
*/
|
|
170
160
|
async deleteOne() {
|
|
171
|
-
const sql_request = `DELETE FROM ${this.
|
|
161
|
+
const sql_request = `DELETE FROM ${this._name} WHERE ${generateCondition(formatObject(this.getRecordData()))}`;
|
|
172
162
|
|
|
163
|
+
logs(sql_request)
|
|
173
164
|
const rows = await getConnexion().promise().execute(sql_request).catch((err) => {
|
|
174
165
|
error(`Error executing query deleteOne: ${err}`);
|
|
175
166
|
throw err;
|
|
@@ -192,7 +183,7 @@ class ModelInstance {
|
|
|
192
183
|
|
|
193
184
|
if (rows[0].length == 0) return 0;
|
|
194
185
|
|
|
195
|
-
return new ModelInstance(this.
|
|
186
|
+
return new ModelInstance(this._name, rows[0], this._schema)._data;
|
|
196
187
|
}
|
|
197
188
|
}
|
|
198
189
|
|
package/src/utils/buildQuery.js
CHANGED
|
@@ -4,7 +4,7 @@ const { escapeIdentifier, escapeOrderDirection, escapeValue } = require("./sql")
|
|
|
4
4
|
|
|
5
5
|
function buildGroupByItem(group) {
|
|
6
6
|
if (typeof group !== 'string') {
|
|
7
|
-
|
|
7
|
+
throw new Error("Group by items must be strings");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const trimmedGroup = group.trim();
|
|
@@ -37,6 +37,10 @@ function buildField(field) {
|
|
|
37
37
|
}
|
|
38
38
|
else if (field.col)
|
|
39
39
|
sql = escapeIdentifier(field.col);
|
|
40
|
+
else if (field.distinct)
|
|
41
|
+
sql = `DISTINCT ${escapeIdentifier(field.distinct)}`;
|
|
42
|
+
else if (field.count)
|
|
43
|
+
sql = `COUNT(${escapeIdentifier(field.count)})`;
|
|
40
44
|
|
|
41
45
|
if (field.as)
|
|
42
46
|
sql += ` AS ${escapeIdentifier(field.as)}`;
|
|
@@ -4,6 +4,7 @@ module.exports = function (obj) {
|
|
|
4
4
|
for (const key in obj) {
|
|
5
5
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
6
6
|
const value = getSafe(obj, key);
|
|
7
|
+
|
|
7
8
|
if (value instanceof Date) {
|
|
8
9
|
// convert Date to MySQL DATETIME (no timezone)
|
|
9
10
|
setSafe(obj, key, value.toISOString().slice(0, 19).replace('T', ' '));
|
|
@@ -37,9 +37,6 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
37
37
|
// normalize strings that may contain surrounding quotes or escaped quotes
|
|
38
38
|
if (typeof value === 'string') {
|
|
39
39
|
value = value.trim();
|
|
40
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
41
|
-
value = value.slice(1, -1);
|
|
42
|
-
}
|
|
43
40
|
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
44
41
|
}
|
|
45
42
|
if (Array.isArray(value)) {
|
|
@@ -67,9 +64,6 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
67
64
|
|
|
68
65
|
if (typeof value === 'string') {
|
|
69
66
|
value = value.trim();
|
|
70
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
71
|
-
value = value.slice(1, -1);
|
|
72
|
-
}
|
|
73
67
|
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
74
68
|
}
|
|
75
69
|
|
|
@@ -92,14 +86,12 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
92
86
|
if (fieldDef) {
|
|
93
87
|
if (fieldDef.type && fieldDef.type.name !== undefined) fieldType = fieldDef.type.name;
|
|
94
88
|
else if (fieldDef.type !== undefined) fieldType = fieldDef.type;
|
|
95
|
-
else if (fieldDef && fieldDef.name !== undefined) fieldType = fieldDef.name;
|
|
96
89
|
}
|
|
97
90
|
const normalizedFieldType = String(fieldType ?? "").toLowerCase();
|
|
98
91
|
const isDateLike = ["date", "datetime", "timestamp", "now"].includes(normalizedFieldType);
|
|
99
92
|
|
|
100
93
|
if (typeof value === "string") {
|
|
101
94
|
let val = value;
|
|
102
|
-
// strip surrounding quotes if any (double safety)
|
|
103
95
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1,-1);
|
|
104
96
|
// if ISO timestamp with Z, convert to MySQL DATETIME format
|
|
105
97
|
if (isDateLike && /T/.test(val)) {
|