@carbonorm/carbonnode 6.0.20 → 6.1.1
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 +521 -259
- package/dist/constants/C6Constants.d.ts +342 -338
- package/dist/executors/SqlExecutor.d.ts +1 -0
- package/dist/index.cjs.js +746 -290
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +737 -291
- package/dist/index.esm.js.map +1 -1
- package/dist/orm/builders/AggregateBuilder.d.ts +5 -1
- package/dist/orm/builders/ConditionBuilder.d.ts +2 -3
- package/dist/orm/builders/ExpressionSerializer.d.ts +22 -0
- package/dist/orm/builders/PaginationBuilder.d.ts +4 -6
- package/dist/orm/queryHelpers.d.ts +12 -1
- package/dist/orm/utils/sqlUtils.d.ts +1 -0
- package/dist/types/mysqlTypes.d.ts +6 -1
- package/dist/types/ormInterfaces.d.ts +7 -5
- package/dist/utils/sqlAllowList.d.ts +5 -3
- package/package.json +2 -2
- package/scripts/assets/handlebars/C6.test.ts.handlebars +4 -4
- package/src/__tests__/expressServer.e2e.test.ts +26 -17
- package/src/__tests__/fixtures/c6.fixture.ts +33 -0
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +53 -14
- package/src/__tests__/normalizeSingularRequest.test.ts +26 -8
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
- package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
- package/src/__tests__/sakila-db/C6.sqlAllowList.json +1 -1
- package/src/__tests__/sakila-db/C6.test.ts +4 -4
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +6 -6
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +19 -12
- package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +10 -10
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +4 -4
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +6 -6
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
- package/src/__tests__/sqlAllowList.test.ts +56 -1
- package/src/__tests__/sqlBuilders.complex.test.ts +62 -74
- package/src/__tests__/sqlBuilders.expressions.test.ts +58 -30
- package/src/__tests__/sqlBuilders.test.ts +106 -5
- package/src/constants/C6Constants.ts +3 -1
- package/src/executors/HttpExecutor.ts +2 -1
- package/src/executors/SqlExecutor.ts +29 -4
- package/src/index.ts +1 -0
- package/src/orm/builders/AggregateBuilder.ts +67 -106
- package/src/orm/builders/ConditionBuilder.ts +72 -103
- package/src/orm/builders/ExpressionSerializer.ts +275 -0
- package/src/orm/builders/PaginationBuilder.ts +24 -34
- package/src/orm/queryHelpers.ts +29 -0
- package/src/orm/utils/sqlUtils.ts +172 -4
- package/src/types/mysqlTypes.ts +130 -9
- package/src/types/ormInterfaces.ts +7 -7
- package/src/utils/normalizeSingularRequest.ts +11 -4
- package/src/utils/sqlAllowList.ts +44 -11
|
@@ -30,22 +30,20 @@ describe('Explicit SQL expression grammar', () => {
|
|
|
30
30
|
[C6C.OR]: [
|
|
31
31
|
{
|
|
32
32
|
[C6C.LESS_THAN_OR_EQUAL_TO]: [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
33
|
+
[
|
|
34
|
+
C6C.ST_DISTANCE_SPHERE,
|
|
35
|
+
Property_Units.LOCATION,
|
|
36
|
+
[C6C.ST_GEOMFROMTEXT, [C6C.LIT, 'POINT(39.4972468 -105.0403593)'], 4326],
|
|
37
|
+
],
|
|
39
38
|
5000,
|
|
40
39
|
],
|
|
41
40
|
},
|
|
42
41
|
{
|
|
43
42
|
[C6C.GREATER_THAN]: [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
43
|
+
[
|
|
44
|
+
C6C.ST_AREA,
|
|
45
|
+
[C6C.ST_GEOMFROMTEXT, [C6C.LIT, 'POLYGON((0 0,1 0,1 1,0 1,0 0))'], 4326],
|
|
46
|
+
],
|
|
49
47
|
10000,
|
|
50
48
|
],
|
|
51
49
|
},
|
|
@@ -56,12 +54,17 @@ describe('Explicit SQL expression grammar', () => {
|
|
|
56
54
|
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
57
55
|
|
|
58
56
|
expect(sql).toContain('WHERE');
|
|
59
|
-
expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units.location, ST_GEOMFROMTEXT\(
|
|
60
|
-
expect(sql).toMatch(/ST_AREA\(ST_GEOMFROMTEXT\(
|
|
57
|
+
expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units.location, ST_GEOMFROMTEXT\(\?, 4326\)\)/);
|
|
58
|
+
expect(sql).toMatch(/ST_AREA\(ST_GEOMFROMTEXT\(\?, 4326\)\)/);
|
|
61
59
|
expect(sql).toMatch(/<= \?/);
|
|
62
60
|
expect(sql).toMatch(/> \?/);
|
|
63
61
|
expect(sql).toContain('OR');
|
|
64
|
-
expect(params).toEqual([
|
|
62
|
+
expect(params).toEqual([
|
|
63
|
+
'POINT(39.4972468 -105.0403593)',
|
|
64
|
+
5000,
|
|
65
|
+
'POLYGON((0 0,1 0,1 1,0 1,0 0))',
|
|
66
|
+
10000,
|
|
67
|
+
]);
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
it('supports operator-first 3-tuple conditions in WHERE', () => {
|
|
@@ -85,7 +88,7 @@ describe('Explicit SQL expression grammar', () => {
|
|
|
85
88
|
const qb = new SelectQueryBuilder(config as any, {
|
|
86
89
|
[C6C.SELECT]: [Property_Units.PARCEL_ID],
|
|
87
90
|
[C6C.WHERE]: {
|
|
88
|
-
0: [[C6C.BETWEEN, Parcel_Sales.SALE_DATE, ['2021-01-01', '2021-12-31']]],
|
|
91
|
+
0: [[C6C.BETWEEN, Parcel_Sales.SALE_DATE, [[C6C.LIT, '2021-01-01'], [C6C.LIT, '2021-12-31']]]],
|
|
89
92
|
},
|
|
90
93
|
} as any, false);
|
|
91
94
|
|
|
@@ -94,30 +97,25 @@ describe('Explicit SQL expression grammar', () => {
|
|
|
94
97
|
expect(params.slice(-2)).toEqual(['2021-01-01', '2021-12-31']);
|
|
95
98
|
});
|
|
96
99
|
|
|
97
|
-
it('
|
|
100
|
+
it('supports tuple-based function expressions with literal wrappers', () => {
|
|
98
101
|
const config = buildParcelConfig();
|
|
99
102
|
|
|
100
103
|
const qb = new SelectQueryBuilder(config as any, {
|
|
101
104
|
[C6C.SELECT]: [Property_Units.PARCEL_ID],
|
|
102
105
|
[C6C.WHERE]: {
|
|
103
|
-
|
|
104
|
-
[
|
|
105
|
-
|
|
106
|
-
C6C.LESS_THAN_OR_EQUAL_TO,
|
|
107
|
-
5000,
|
|
108
|
-
],
|
|
106
|
+
[C6C.LESS_THAN_OR_EQUAL_TO]: [
|
|
107
|
+
[C6C.ST_DISTANCE_SPHERE, Property_Units.LOCATION, [C6C.ST_GEOMFROMTEXT, [C6C.LIT, 'POINT(39.4972468 -105.0403593)'], 4326]],
|
|
108
|
+
5000,
|
|
109
109
|
],
|
|
110
110
|
},
|
|
111
111
|
} as any, false);
|
|
112
112
|
|
|
113
113
|
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
114
|
-
expect(sql).toMatch(
|
|
115
|
-
|
|
116
|
-
);
|
|
117
|
-
expect(params.slice(-1)[0]).toBe(5000);
|
|
114
|
+
expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units\.location, ST_GEOMFROMTEXT\(\?, 4326\)\) <= \?/);
|
|
115
|
+
expect(params).toEqual(['POINT(39.4972468 -105.0403593)', 5000]);
|
|
118
116
|
});
|
|
119
117
|
|
|
120
|
-
it('rejects raw function
|
|
118
|
+
it('rejects raw function expression strings', () => {
|
|
121
119
|
const config = buildParcelConfig();
|
|
122
120
|
|
|
123
121
|
const qb = new SelectQueryBuilder(config as any, {
|
|
@@ -133,7 +131,37 @@ describe('Explicit SQL expression grammar', () => {
|
|
|
133
131
|
},
|
|
134
132
|
} as any, false);
|
|
135
133
|
|
|
136
|
-
expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/
|
|
134
|
+
expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Bare string/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('rejects legacy positional AS syntax in function tuples', () => {
|
|
138
|
+
const config = buildParcelConfig();
|
|
139
|
+
|
|
140
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
141
|
+
[C6C.SELECT]: [[C6C.COUNT, Property_Units.PARCEL_ID, C6C.AS, 'cnt']],
|
|
142
|
+
} as any, false);
|
|
143
|
+
|
|
144
|
+
expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Legacy positional AS syntax/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('rejects legacy positional AS syntax in column tuples', () => {
|
|
148
|
+
const config = buildParcelConfig();
|
|
149
|
+
|
|
150
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
151
|
+
[C6C.SELECT]: [[Property_Units.PARCEL_ID, C6C.AS, 'parcel_id_alias']],
|
|
152
|
+
} as any, false);
|
|
153
|
+
|
|
154
|
+
expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Legacy positional AS syntax/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('rejects object-rooted function expressions', () => {
|
|
158
|
+
const config = buildParcelConfig();
|
|
159
|
+
|
|
160
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
161
|
+
[C6C.SELECT]: [{ [C6C.COUNT]: [Property_Units.PARCEL_ID] }],
|
|
162
|
+
} as any, false);
|
|
163
|
+
|
|
164
|
+
expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Object-rooted expressions are not supported/);
|
|
137
165
|
});
|
|
138
166
|
|
|
139
167
|
it('supports explicit AND groupings composed of nested OR clauses', () => {
|
|
@@ -146,8 +174,8 @@ describe('Explicit SQL expression grammar', () => {
|
|
|
146
174
|
{ [C6C.GREATER_THAN]: [Property_Units.PARCEL_ID, 100] },
|
|
147
175
|
{
|
|
148
176
|
[C6C.OR]: [
|
|
149
|
-
{ [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, ['2021-01-01', '2022-06-30']] },
|
|
150
|
-
{ [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, ['2023-01-01', '2024-06-30']] },
|
|
177
|
+
{ [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, [[C6C.LIT, '2021-01-01'], [C6C.LIT, '2022-06-30']]] },
|
|
178
|
+
{ [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, [[C6C.LIT, '2023-01-01'], [C6C.LIT, '2024-06-30']]] },
|
|
151
179
|
],
|
|
152
180
|
},
|
|
153
181
|
],
|
|
@@ -4,14 +4,15 @@ import { SelectQueryBuilder } from '../orm/queries/SelectQueryBuilder';
|
|
|
4
4
|
import { PostQueryBuilder } from '../orm/queries/PostQueryBuilder';
|
|
5
5
|
import { UpdateQueryBuilder } from '../orm/queries/UpdateQueryBuilder';
|
|
6
6
|
import { DeleteQueryBuilder } from '../orm/queries/DeleteQueryBuilder';
|
|
7
|
-
import {
|
|
7
|
+
import { alias, call, distinct, fn, lit, order } from '../orm/queryHelpers';
|
|
8
|
+
import { buildTestConfig, buildBinaryTestConfig, buildBinaryTestConfigFqn, buildTemporalTestConfig } from './fixtures/c6.fixture';
|
|
8
9
|
|
|
9
10
|
describe('SQL Builders', () => {
|
|
10
11
|
it('builds SELECT with JOIN, WHERE, GROUP BY, HAVING and default LIMIT', () => {
|
|
11
12
|
const config = buildTestConfig();
|
|
12
13
|
// named params disabled -> positional params array
|
|
13
14
|
const qb = new SelectQueryBuilder(config as any, {
|
|
14
|
-
SELECT: ['actor.first_name', [C6C.COUNT, 'actor.actor_id',
|
|
15
|
+
SELECT: ['actor.first_name', [C6C.AS, [C6C.COUNT, 'actor.actor_id'], 'cnt']],
|
|
15
16
|
JOIN: {
|
|
16
17
|
[C6C.INNER]: {
|
|
17
18
|
'film_actor fa': {
|
|
@@ -20,7 +21,7 @@ describe('SQL Builders', () => {
|
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
23
|
WHERE: {
|
|
23
|
-
'actor.first_name': [C6C.LIKE, '%A%'],
|
|
24
|
+
'actor.first_name': [C6C.LIKE, [C6C.LIT, '%A%']],
|
|
24
25
|
0: {
|
|
25
26
|
'actor.actor_id': [C6C.GREATER_THAN, 10],
|
|
26
27
|
}
|
|
@@ -47,7 +48,7 @@ describe('SQL Builders', () => {
|
|
|
47
48
|
const config = buildTestConfig();
|
|
48
49
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
49
50
|
const qb = new SelectQueryBuilder(config as any, {
|
|
50
|
-
SELECT: [[C6C.COUNT, 'actor.actor_id',
|
|
51
|
+
SELECT: [[C6C.AS, [C6C.COUNT, 'actor.actor_id'], 'cnt']],
|
|
51
52
|
} as any, false);
|
|
52
53
|
|
|
53
54
|
qb.build('actor');
|
|
@@ -62,6 +63,39 @@ describe('SQL Builders', () => {
|
|
|
62
63
|
logSpy.mockRestore();
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
it('supports custom functions through C6C.CALL and binds string literals', () => {
|
|
67
|
+
const config = buildTestConfig();
|
|
68
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
69
|
+
SELECT: [[C6C.AS, [C6C.CALL, 'COALESCE', [C6C.LIT, 'fallback'], 'actor.first_name'], 'resolved_name']],
|
|
70
|
+
} as any, false);
|
|
71
|
+
|
|
72
|
+
const { sql, params } = qb.build('actor');
|
|
73
|
+
expect(sql).toContain('COALESCE(?, actor.first_name) AS resolved_name');
|
|
74
|
+
expect(params).toEqual(['fallback']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('supports helper builders for fn/call/as/distinct/lit/order', () => {
|
|
78
|
+
const config = buildTestConfig();
|
|
79
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
80
|
+
SELECT: [
|
|
81
|
+
alias(distinct('actor.first_name'), 'distinct_name'),
|
|
82
|
+
alias(fn(C6C.COUNT, 'actor.actor_id'), 'cnt'),
|
|
83
|
+
call('COALESCE', lit('N/A'), 'actor.last_name'),
|
|
84
|
+
],
|
|
85
|
+
PAGINATION: {
|
|
86
|
+
[C6C.ORDER]: [order(fn(C6C.COUNT, 'actor.actor_id'), 'DESC')],
|
|
87
|
+
[C6C.LIMIT]: 5,
|
|
88
|
+
},
|
|
89
|
+
} as any, false);
|
|
90
|
+
|
|
91
|
+
const { sql, params } = qb.build('actor');
|
|
92
|
+
expect(sql).toContain('DISTINCT actor.first_name AS distinct_name');
|
|
93
|
+
expect(sql).toContain('COUNT(actor.actor_id) AS cnt');
|
|
94
|
+
expect(sql).toContain('COALESCE(?, actor.last_name)');
|
|
95
|
+
expect(sql).toContain('ORDER BY COUNT(actor.actor_id) DESC');
|
|
96
|
+
expect(params).toEqual(['N/A']);
|
|
97
|
+
});
|
|
98
|
+
|
|
65
99
|
it('builds INSERT with ON DUPLICATE KEY UPDATE', () => {
|
|
66
100
|
const config = buildTestConfig();
|
|
67
101
|
const qb = new PostQueryBuilder(config as any, {
|
|
@@ -192,6 +226,36 @@ describe('SQL Builders', () => {
|
|
|
192
226
|
expect(params).toEqual(['ALICE', 5]);
|
|
193
227
|
});
|
|
194
228
|
|
|
229
|
+
it('supports expression tuples in UPDATE values', () => {
|
|
230
|
+
const config = buildTestConfig();
|
|
231
|
+
const qb = new UpdateQueryBuilder(config as any, {
|
|
232
|
+
[C6C.UPDATE]: {
|
|
233
|
+
'actor.first_name': [C6C.CONCAT, [C6C.LIT, 'Mr. '], 'actor.last_name'],
|
|
234
|
+
},
|
|
235
|
+
WHERE: {
|
|
236
|
+
'actor.actor_id': [C6C.EQUAL, 7],
|
|
237
|
+
},
|
|
238
|
+
} as any, false);
|
|
239
|
+
|
|
240
|
+
const { sql, params } = qb.build('actor');
|
|
241
|
+
expect(sql).toContain('`first_name` = CONCAT(?, actor.last_name)');
|
|
242
|
+
expect(params).toEqual(['Mr. ', 7]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('supports expression tuples in INSERT values', () => {
|
|
246
|
+
const config = buildTestConfig();
|
|
247
|
+
const qb = new PostQueryBuilder(config as any, {
|
|
248
|
+
[C6C.INSERT]: {
|
|
249
|
+
'actor.first_name': [C6C.CONCAT, [C6C.LIT, 'HEL'], [C6C.LIT, 'LO']],
|
|
250
|
+
'actor.last_name': 'SMITH',
|
|
251
|
+
},
|
|
252
|
+
} as any, false);
|
|
253
|
+
|
|
254
|
+
const { sql, params } = qb.build('actor');
|
|
255
|
+
expect(sql).toContain('CONCAT(?, ?)');
|
|
256
|
+
expect(params).toEqual(['HEL', 'LO', 'SMITH']);
|
|
257
|
+
});
|
|
258
|
+
|
|
195
259
|
it('builds UPDATE when columns are fully qualified', () => {
|
|
196
260
|
const config = buildTestConfig();
|
|
197
261
|
const qb = new UpdateQueryBuilder(config as any, {
|
|
@@ -235,7 +299,7 @@ describe('SQL Builders', () => {
|
|
|
235
299
|
const config = buildTestConfig();
|
|
236
300
|
const qb = new SelectQueryBuilder(config as any, {
|
|
237
301
|
WHERE: {
|
|
238
|
-
'actor.binarycol': [C6C.EQUAL, '0123456789abcdef0123456789abcdef']
|
|
302
|
+
'actor.binarycol': [C6C.EQUAL, [C6C.LIT, '0123456789abcdef0123456789abcdef']]
|
|
239
303
|
}
|
|
240
304
|
} as any, false);
|
|
241
305
|
|
|
@@ -317,4 +381,41 @@ describe('SQL Builders', () => {
|
|
|
317
381
|
expect(Buffer.isBuffer(buf)).toBe(true);
|
|
318
382
|
expect((buf as Buffer).length).toBe(16);
|
|
319
383
|
});
|
|
384
|
+
|
|
385
|
+
it('serializes ISO-8601 strings for TIMESTAMP columns in INSERT params', () => {
|
|
386
|
+
const config = buildTemporalTestConfig();
|
|
387
|
+
const qb = new PostQueryBuilder(config as any, {
|
|
388
|
+
[C6C.INSERT]: {
|
|
389
|
+
'events.read_at': '2026-02-16T21:27:06.679Z'
|
|
390
|
+
}
|
|
391
|
+
} as any, false);
|
|
392
|
+
|
|
393
|
+
const { params } = qb.build('events');
|
|
394
|
+
expect(params).toEqual(['2026-02-16 21:27:06.679']);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('serializes ISO-8601 strings for DATE columns in UPDATE params', () => {
|
|
398
|
+
const config = buildTemporalTestConfig();
|
|
399
|
+
const qb = new UpdateQueryBuilder(config as any, {
|
|
400
|
+
[C6C.UPDATE]: {
|
|
401
|
+
'events.read_on': '2026-02-16T21:27:06.679Z'
|
|
402
|
+
},
|
|
403
|
+
WHERE: { 'events.id': [C6C.EQUAL, 1] }
|
|
404
|
+
} as any, false);
|
|
405
|
+
|
|
406
|
+
const { params } = qb.build('events');
|
|
407
|
+
expect(params).toEqual(['2026-02-16', 1]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('serializes offset ISO-8601 strings for TIME columns in WHERE params', () => {
|
|
411
|
+
const config = buildTemporalTestConfig();
|
|
412
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
413
|
+
WHERE: {
|
|
414
|
+
'events.read_time': [C6C.EQUAL, [C6C.LIT, '2026-02-16T16:27:06.679-05:00']]
|
|
415
|
+
}
|
|
416
|
+
} as any, false);
|
|
417
|
+
|
|
418
|
+
const { params } = qb.build('events');
|
|
419
|
+
expect(params).toEqual(['21:27:06.679']);
|
|
420
|
+
});
|
|
320
421
|
});
|
|
@@ -9,6 +9,7 @@ export const C6Constants = {
|
|
|
9
9
|
BETWEEN: 'BETWEEN',
|
|
10
10
|
|
|
11
11
|
CONCAT: 'CONCAT',
|
|
12
|
+
CALL: 'CALL',
|
|
12
13
|
CONVERT_TZ: 'CONVERT_TZ',
|
|
13
14
|
COUNT: 'COUNT',
|
|
14
15
|
COUNT_ALL: 'COUNT_ALL',
|
|
@@ -71,6 +72,7 @@ export const C6Constants = {
|
|
|
71
72
|
LESS_THAN: '<',
|
|
72
73
|
LESS_THAN_OR_EQUAL_TO: '<=',
|
|
73
74
|
LIKE: 'LIKE',
|
|
75
|
+
LIT: 'LIT',
|
|
74
76
|
LIMIT: 'LIMIT',
|
|
75
77
|
LOCALTIME: 'LOCALTIME',
|
|
76
78
|
LOCALTIMESTAMP: 'LOCALTIMESTAMP',
|
|
@@ -198,7 +200,7 @@ export const C6Constants = {
|
|
|
198
200
|
FINISH: 'FINISH',
|
|
199
201
|
VALIDATE_C6_ENTITY_ID_REGEX: '#^([a-fA-F0-9]{20,35})$#',
|
|
200
202
|
|
|
201
|
-
};
|
|
203
|
+
} as const;
|
|
202
204
|
|
|
203
205
|
|
|
204
206
|
export const C6C = C6Constants;
|
|
@@ -209,6 +209,7 @@ export class HttpExecutor<
|
|
|
209
209
|
const {
|
|
210
210
|
debug,
|
|
211
211
|
cacheResults = (C6.GET === requestMethod),
|
|
212
|
+
skipReactBootstrap = false,
|
|
212
213
|
dataInsertMultipleRows,
|
|
213
214
|
success,
|
|
214
215
|
fetchDependencies = eFetchDependencies.NONE,
|
|
@@ -557,7 +558,7 @@ export class HttpExecutor<
|
|
|
557
558
|
response
|
|
558
559
|
});
|
|
559
560
|
|
|
560
|
-
if (undefined !== reactBootstrap && response) {
|
|
561
|
+
if (undefined !== reactBootstrap && response && !skipReactBootstrap) {
|
|
561
562
|
switch (requestMethod) {
|
|
562
563
|
case GET:
|
|
563
564
|
response.data && reactBootstrap.updateRestfulObjectArrays<G['RestTableInterface']>({
|
|
@@ -22,7 +22,7 @@ import logSql, {
|
|
|
22
22
|
} from "../utils/logSql";
|
|
23
23
|
import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
|
|
24
24
|
import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
|
|
25
|
-
import { loadSqlAllowList,
|
|
25
|
+
import { loadSqlAllowList, normalizeSqlWith } from "../utils/sqlAllowList";
|
|
26
26
|
import { getLogContext, LogLevel, logWithLevel } from "../utils/logLevel";
|
|
27
27
|
|
|
28
28
|
const SQL_ALLOWLIST_BLOCKED_CODE = "SQL_ALLOWLIST_BLOCKED";
|
|
@@ -438,6 +438,7 @@ export class SqlExecutor<
|
|
|
438
438
|
C6C.REPLACE,
|
|
439
439
|
"dataInsertMultipleRows",
|
|
440
440
|
"cacheResults",
|
|
441
|
+
"skipReactBootstrap",
|
|
441
442
|
"fetchDependencies",
|
|
442
443
|
"debug",
|
|
443
444
|
"success",
|
|
@@ -530,7 +531,7 @@ export class SqlExecutor<
|
|
|
530
531
|
}
|
|
531
532
|
|
|
532
533
|
if (value !== undefined) {
|
|
533
|
-
pkValues[pkShort] = value;
|
|
534
|
+
pkValues[pkShort] = this.unwrapPrimaryKeyValue(value);
|
|
534
535
|
}
|
|
535
536
|
}
|
|
536
537
|
|
|
@@ -541,6 +542,29 @@ export class SqlExecutor<
|
|
|
541
542
|
return Object.keys(pkValues).length > 0 ? pkValues : null;
|
|
542
543
|
}
|
|
543
544
|
|
|
545
|
+
private unwrapPrimaryKeyValue(value: any): any {
|
|
546
|
+
if (!Array.isArray(value)) return value;
|
|
547
|
+
|
|
548
|
+
if (value.length === 2) {
|
|
549
|
+
const [head, tail] = value;
|
|
550
|
+
if (head === C6C.EQUAL) {
|
|
551
|
+
return this.unwrapPrimaryKeyValue(tail);
|
|
552
|
+
}
|
|
553
|
+
if (head === C6C.LIT || head === C6C.PARAM) {
|
|
554
|
+
return tail;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (value.length === 3) {
|
|
559
|
+
const [, operator, right] = value;
|
|
560
|
+
if (operator === C6C.EQUAL) {
|
|
561
|
+
return this.unwrapPrimaryKeyValue(right);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
|
|
544
568
|
private extractPrimaryKeyValuesFromData(data: any): Record<string, any> | null {
|
|
545
569
|
if (!data) return null;
|
|
546
570
|
const row = Array.isArray(data) ? data[0] : data;
|
|
@@ -1046,8 +1070,9 @@ export class SqlExecutor<
|
|
|
1046
1070
|
return "not verified";
|
|
1047
1071
|
}
|
|
1048
1072
|
|
|
1049
|
-
const
|
|
1050
|
-
const
|
|
1073
|
+
const sqlQueryNormalizer = this.config.sqlQueryNormalizer;
|
|
1074
|
+
const allowList = await loadSqlAllowList(allowListPath, sqlQueryNormalizer);
|
|
1075
|
+
const normalized = normalizeSqlWith(sql, sqlQueryNormalizer);
|
|
1051
1076
|
if (!allowList.has(normalized)) {
|
|
1052
1077
|
throw createSqlAllowListBlockedError({
|
|
1053
1078
|
tableName:
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./handlers/ExpressHandler";
|
|
|
19
19
|
export * from "./orm/queryHelpers";
|
|
20
20
|
export * from "./orm/builders/AggregateBuilder";
|
|
21
21
|
export * from "./orm/builders/ConditionBuilder";
|
|
22
|
+
export * from "./orm/builders/ExpressionSerializer";
|
|
22
23
|
export * from "./orm/builders/JoinBuilder";
|
|
23
24
|
export * from "./orm/builders/PaginationBuilder";
|
|
24
25
|
export * from "./orm/queries/DeleteQueryBuilder";
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import {Executor} from "../../executors/Executor";
|
|
2
2
|
import {OrmGenerics} from "../../types/ormGenerics";
|
|
3
|
-
import {
|
|
3
|
+
import {SQL_KNOWN_FUNCTIONS} from "../../types/mysqlTypes";
|
|
4
4
|
import {getLogContext, LogLevel, logWithLevel} from "../../utils/logLevel";
|
|
5
|
+
import {
|
|
6
|
+
iSerializedExpression,
|
|
7
|
+
serializeSqlExpression,
|
|
8
|
+
tSqlParams,
|
|
9
|
+
} from "./ExpressionSerializer";
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
const KNOWN_FUNCTION_LOOKUP = new Set(
|
|
12
|
+
SQL_KNOWN_FUNCTIONS.map((name) => String(name).toUpperCase()),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G> {
|
|
7
16
|
protected selectAliases: Set<string> = new Set<string>();
|
|
8
17
|
|
|
9
18
|
// Overridden in ConditionBuilder where alias tracking is available.
|
|
@@ -12,130 +21,82 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
12
21
|
// no-op placeholder for subclasses that do not implement alias validation
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
if (typeof
|
|
17
|
-
this.assertValidIdentifier(field, 'SELECT field');
|
|
18
|
-
return field;
|
|
19
|
-
}
|
|
24
|
+
protected isReferenceExpression(value: string): boolean {
|
|
25
|
+
if (typeof value !== 'string') return false;
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// If the array represents a tuple/literal list (e.g., [lng, lat]) rather than a
|
|
26
|
-
// function call like [FN, ...args], serialize the list as a comma-separated
|
|
27
|
-
// literal sequence so parent calls (like ORDER BY FN(<here>)) can embed it.
|
|
28
|
-
const isNumericString = (s: string) => /^-?\d+(?:\.\d+)?$/.test(String(s).trim());
|
|
29
|
-
if (typeof field[0] !== 'string' || isNumericString(field[0])) {
|
|
30
|
-
return field
|
|
31
|
-
.map((arg) => {
|
|
32
|
-
if (Array.isArray(arg)) return this.buildAggregateField(arg, params);
|
|
33
|
-
return String(arg);
|
|
34
|
-
})
|
|
35
|
-
.join(', ');
|
|
36
|
-
}
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (trimmed.length === 0) return false;
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
let alias: string | undefined;
|
|
30
|
+
if (trimmed === '*') return true;
|
|
40
31
|
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*\.\*$/.test(trimmed)) {
|
|
33
|
+
this.assertValidIdentifier(trimmed, 'SQL reference');
|
|
34
|
+
return true;
|
|
44
35
|
}
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (args.length === 1 && Array.isArray(args[0])) {
|
|
50
|
-
args = args[0];
|
|
37
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
|
|
38
|
+
this.assertValidIdentifier(trimmed, 'SQL reference');
|
|
39
|
+
return true;
|
|
51
40
|
}
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!params) {
|
|
56
|
-
throw new Error('PARAM requires parameter tracking.');
|
|
57
|
-
}
|
|
58
|
-
const value = args[0];
|
|
59
|
-
// Use empty column context; ORDER/SELECT literals have no column typing.
|
|
60
|
-
// @ts-ignore addParam is provided by ConditionBuilder in our hierarchy.
|
|
61
|
-
return this.addParam(params, '', value);
|
|
42
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed) && this.selectAliases.has(trimmed)) {
|
|
43
|
+
return true;
|
|
62
44
|
}
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
throw new Error('Scalar subselects in SELECT require parameter tracking.');
|
|
67
|
-
}
|
|
68
|
-
const subRequest = args[0];
|
|
69
|
-
const subSql = (this as any).buildScalarSubSelect?.(subRequest, params);
|
|
70
|
-
if (!subSql) {
|
|
71
|
-
throw new Error('Failed to build scalar subselect.');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
let expr = subSql;
|
|
75
|
-
if (alias) {
|
|
76
|
-
this.selectAliases.add(alias);
|
|
77
|
-
expr += ` AS ${alias}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
logWithLevel(
|
|
81
|
-
LogLevel.DEBUG,
|
|
82
|
-
getLogContext(this.config, this.request),
|
|
83
|
-
console.log,
|
|
84
|
-
`[SELECT] ${expr}`,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
return expr;
|
|
88
|
-
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
89
48
|
|
|
90
|
-
|
|
49
|
+
protected isKnownFunction(functionName: string): boolean {
|
|
50
|
+
return KNOWN_FUNCTION_LOOKUP.has(functionName.trim().toUpperCase());
|
|
51
|
+
}
|
|
91
52
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
53
|
+
protected serializeExpression(
|
|
54
|
+
expression: any,
|
|
55
|
+
params?: tSqlParams,
|
|
56
|
+
context: string = 'SQL expression',
|
|
57
|
+
contextColumn?: string,
|
|
58
|
+
): iSerializedExpression {
|
|
59
|
+
return serializeSqlExpression(expression, {
|
|
60
|
+
params,
|
|
61
|
+
context,
|
|
62
|
+
contextColumn,
|
|
63
|
+
hooks: {
|
|
64
|
+
assertValidIdentifier: (identifier: string, hookContext: string) => {
|
|
65
|
+
this.assertValidIdentifier(identifier, hookContext);
|
|
66
|
+
},
|
|
67
|
+
isReference: (value: string) => this.isReferenceExpression(value),
|
|
68
|
+
addParam: (target: tSqlParams, column: string, value: any) => {
|
|
69
|
+
const addParam = (this as any).addParam;
|
|
70
|
+
if (typeof addParam !== 'function') {
|
|
71
|
+
throw new Error('Expression literal binding requires addParam support.');
|
|
99
72
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (alreadyQuoted) {
|
|
107
|
-
return trimmed;
|
|
108
|
-
}
|
|
109
|
-
const escaped = arg.replace(/'/g, "''");
|
|
110
|
-
return `'${escaped}'`;
|
|
73
|
+
return addParam.call(this, target, column, value);
|
|
74
|
+
},
|
|
75
|
+
buildScalarSubSelect: (subRequest: any, target: tSqlParams) => {
|
|
76
|
+
const builder = (this as any).buildScalarSubSelect;
|
|
77
|
+
if (typeof builder !== 'function') {
|
|
78
|
+
throw new Error('Scalar subselects require SelectQueryBuilder context.');
|
|
111
79
|
}
|
|
80
|
+
return builder.call(this, subRequest, target);
|
|
81
|
+
},
|
|
82
|
+
onAlias: (alias: string) => {
|
|
83
|
+
this.selectAliases.add(alias);
|
|
84
|
+
},
|
|
85
|
+
isKnownFunction: (functionName: string) => this.isKnownFunction(functionName),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return String(arg);
|
|
116
|
-
})
|
|
117
|
-
.join(', ');
|
|
118
|
-
|
|
119
|
-
let expr: string;
|
|
120
|
-
|
|
121
|
-
if (F === 'DISTINCT') {
|
|
122
|
-
expr = `DISTINCT ${argList}`;
|
|
123
|
-
} else {
|
|
124
|
-
expr = `${F}(${argList})`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (alias) {
|
|
128
|
-
this.selectAliases.add(alias);
|
|
129
|
-
expr += ` AS ${alias}`;
|
|
130
|
-
}
|
|
90
|
+
buildAggregateField(field: string | any[], params?: tSqlParams): string {
|
|
91
|
+
const serialized = this.serializeExpression(field, params, 'SELECT expression');
|
|
131
92
|
|
|
132
93
|
logWithLevel(
|
|
133
94
|
LogLevel.DEBUG,
|
|
134
95
|
getLogContext(this.config, this.request),
|
|
135
96
|
console.log,
|
|
136
|
-
`[SELECT] ${
|
|
97
|
+
`[SELECT] ${serialized.sql}`,
|
|
137
98
|
);
|
|
138
99
|
|
|
139
|
-
return
|
|
100
|
+
return serialized.sql;
|
|
140
101
|
}
|
|
141
102
|
}
|