@carbonorm/carbonnode 6.0.19 → 6.1.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 +46 -1
- package/dist/constants/C6Constants.d.ts +342 -338
- package/dist/executors/SqlExecutor.d.ts +8 -0
- package/dist/index.cjs.js +751 -272
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +744 -273
- 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/types/mysqlTypes.d.ts +6 -1
- package/dist/types/ormInterfaces.d.ts +7 -5
- package/dist/utils/cacheManager.d.ts +3 -2
- package/package.json +2 -2
- package/scripts/assets/handlebars/C6.test.ts.handlebars +4 -4
- package/src/__tests__/cacheManager.test.ts +28 -0
- package/src/__tests__/expressServer.e2e.test.ts +26 -17
- 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 +11 -4
- 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 +26 -7
- 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 +9 -3
- 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 +10 -3
- 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 +9 -3
- 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 +18 -6
- 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 +18 -3
- 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 +9 -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 +9 -3
- 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 +13 -3
- 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.join.json +10 -10
- package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +14 -4
- 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__/sqlBuilders.complex.test.ts +62 -74
- package/src/__tests__/sqlBuilders.expressions.test.ts +58 -30
- package/src/__tests__/sqlBuilders.test.ts +68 -4
- package/src/__tests__/sqlExecutorPostUuid.test.ts +185 -0
- package/src/constants/C6Constants.ts +3 -1
- package/src/executors/HttpExecutor.ts +35 -6
- package/src/executors/SqlExecutor.ts +232 -4
- package/src/index.ts +1 -0
- package/src/orm/builders/AggregateBuilder.ts +67 -106
- package/src/orm/builders/ConditionBuilder.ts +69 -93
- 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/types/mysqlTypes.ts +130 -9
- package/src/types/ormInterfaces.ts +7 -7
- package/src/utils/cacheManager.ts +6 -4
- package/src/utils/normalizeSingularRequest.ts +11 -4
|
@@ -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,6 +4,7 @@ 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 { alias, call, distinct, fn, lit, order } from '../orm/queryHelpers';
|
|
7
8
|
import { buildTestConfig, buildBinaryTestConfig, buildBinaryTestConfigFqn } from './fixtures/c6.fixture';
|
|
8
9
|
|
|
9
10
|
describe('SQL Builders', () => {
|
|
@@ -11,7 +12,7 @@ describe('SQL Builders', () => {
|
|
|
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
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { SqlExecutor } from "../executors/SqlExecutor";
|
|
3
|
+
|
|
4
|
+
function buildLifecycleHooks() {
|
|
5
|
+
return { GET: {}, POST: {}, PUT: {}, DELETE: {} } as any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function buildUuidPrimaryConfig(conn: any) {
|
|
9
|
+
const restModel: any = {
|
|
10
|
+
TABLE_NAME: "report_dashboards",
|
|
11
|
+
PRIMARY: ["report_dashboards.dashboard_id"],
|
|
12
|
+
PRIMARY_SHORT: ["dashboard_id"],
|
|
13
|
+
COLUMNS: {
|
|
14
|
+
"report_dashboards.dashboard_id": "dashboard_id",
|
|
15
|
+
"report_dashboards.title": "title",
|
|
16
|
+
"report_dashboards.created_by": "created_by",
|
|
17
|
+
},
|
|
18
|
+
TYPE_VALIDATION: {
|
|
19
|
+
"report_dashboards.dashboard_id": {
|
|
20
|
+
MYSQL_TYPE: "binary",
|
|
21
|
+
MAX_LENGTH: "16",
|
|
22
|
+
AUTO_INCREMENT: false,
|
|
23
|
+
SKIP_COLUMN_IN_POST: false,
|
|
24
|
+
},
|
|
25
|
+
"report_dashboards.title": {
|
|
26
|
+
MYSQL_TYPE: "varchar",
|
|
27
|
+
MAX_LENGTH: "120",
|
|
28
|
+
AUTO_INCREMENT: false,
|
|
29
|
+
SKIP_COLUMN_IN_POST: false,
|
|
30
|
+
},
|
|
31
|
+
"report_dashboards.created_by": {
|
|
32
|
+
MYSQL_TYPE: "varchar",
|
|
33
|
+
MAX_LENGTH: "255",
|
|
34
|
+
AUTO_INCREMENT: false,
|
|
35
|
+
SKIP_COLUMN_IN_POST: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
LIFECYCLE_HOOKS: buildLifecycleHooks(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
requestMethod: "POST",
|
|
43
|
+
mysqlPool: {
|
|
44
|
+
getConnection: vi.fn(async () => conn),
|
|
45
|
+
},
|
|
46
|
+
C6: {
|
|
47
|
+
PREFIX: "",
|
|
48
|
+
TABLES: {
|
|
49
|
+
report_dashboards: restModel,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
restModel,
|
|
53
|
+
} as any;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildAutoIncrementConfig(conn: any) {
|
|
57
|
+
const restModel: any = {
|
|
58
|
+
TABLE_NAME: "widgets",
|
|
59
|
+
PRIMARY: ["widgets.widget_id"],
|
|
60
|
+
PRIMARY_SHORT: ["widget_id"],
|
|
61
|
+
COLUMNS: {
|
|
62
|
+
"widgets.widget_id": "widget_id",
|
|
63
|
+
"widgets.name": "name",
|
|
64
|
+
},
|
|
65
|
+
TYPE_VALIDATION: {
|
|
66
|
+
"widgets.widget_id": {
|
|
67
|
+
MYSQL_TYPE: "int",
|
|
68
|
+
MAX_LENGTH: "11",
|
|
69
|
+
AUTO_INCREMENT: true,
|
|
70
|
+
SKIP_COLUMN_IN_POST: false,
|
|
71
|
+
},
|
|
72
|
+
"widgets.name": {
|
|
73
|
+
MYSQL_TYPE: "varchar",
|
|
74
|
+
MAX_LENGTH: "120",
|
|
75
|
+
AUTO_INCREMENT: false,
|
|
76
|
+
SKIP_COLUMN_IN_POST: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
LIFECYCLE_HOOKS: buildLifecycleHooks(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
requestMethod: "POST",
|
|
84
|
+
mysqlPool: {
|
|
85
|
+
getConnection: vi.fn(async () => conn),
|
|
86
|
+
},
|
|
87
|
+
C6: {
|
|
88
|
+
PREFIX: "",
|
|
89
|
+
TABLES: {
|
|
90
|
+
widgets: restModel,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
restModel,
|
|
94
|
+
} as any;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildWriteConn(affectedRows = 1, insertId = 0) {
|
|
98
|
+
return {
|
|
99
|
+
beginTransaction: vi.fn(async () => undefined),
|
|
100
|
+
query: vi.fn(async () => [{ affectedRows, insertId }, []]),
|
|
101
|
+
commit: vi.fn(async () => undefined),
|
|
102
|
+
rollback: vi.fn(async () => undefined),
|
|
103
|
+
release: vi.fn(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function expectUuidV7Hex(value: unknown) {
|
|
108
|
+
expect(typeof value).toBe("string");
|
|
109
|
+
const hex = String(value).toUpperCase();
|
|
110
|
+
expect(hex).toMatch(/^[0-9A-F]{32}$/);
|
|
111
|
+
expect(hex[12]).toBe("7");
|
|
112
|
+
expect(hex[16]).toMatch(/[89AB]/);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe("SqlExecutor POST UUID synthesis", () => {
|
|
116
|
+
it("generates missing UUID primary keys for POST and populates rest payload", async () => {
|
|
117
|
+
const conn = buildWriteConn(1, 0);
|
|
118
|
+
const config = buildUuidPrimaryConfig(conn);
|
|
119
|
+
const request: any = {
|
|
120
|
+
title: "Board One",
|
|
121
|
+
created_by: "user-1",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const executor = new SqlExecutor<any>(config, request);
|
|
125
|
+
const result: any = await executor.execute();
|
|
126
|
+
|
|
127
|
+
expect(Array.isArray(result.rest)).toBe(true);
|
|
128
|
+
expect(result.rest).toHaveLength(1);
|
|
129
|
+
expect(result.rest[0]).toMatchObject({
|
|
130
|
+
title: "Board One",
|
|
131
|
+
created_by: "user-1",
|
|
132
|
+
});
|
|
133
|
+
expectUuidV7Hex(result.rest[0].dashboard_id);
|
|
134
|
+
|
|
135
|
+
const [sql, values] = conn.query.mock.calls[0];
|
|
136
|
+
expect(sql).toContain("INSERT INTO `report_dashboards`");
|
|
137
|
+
const uuidBuffers = (values as any[]).filter((value) => Buffer.isBuffer(value));
|
|
138
|
+
expect(uuidBuffers).toHaveLength(1);
|
|
139
|
+
expect(uuidBuffers[0]).toHaveLength(16);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("generates UUIDs for each multi-row insert payload row", async () => {
|
|
143
|
+
const conn = buildWriteConn(2, 0);
|
|
144
|
+
const config = buildUuidPrimaryConfig(conn);
|
|
145
|
+
const request: any = {
|
|
146
|
+
dataInsertMultipleRows: [
|
|
147
|
+
{ title: "One", created_by: "user-1" },
|
|
148
|
+
{ title: "Two", created_by: "user-1" },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const executor = new SqlExecutor<any>(config, request);
|
|
153
|
+
const result: any = await executor.execute();
|
|
154
|
+
|
|
155
|
+
expect(Array.isArray(result.rest)).toBe(true);
|
|
156
|
+
expect(result.rest).toHaveLength(2);
|
|
157
|
+
expectUuidV7Hex(result.rest[0].dashboard_id);
|
|
158
|
+
expectUuidV7Hex(result.rest[1].dashboard_id);
|
|
159
|
+
expect(result.rest[0].dashboard_id).not.toBe(result.rest[1].dashboard_id);
|
|
160
|
+
|
|
161
|
+
const [, values] = conn.query.mock.calls[0];
|
|
162
|
+
const uuidBuffers = (values as any[]).filter((value) => Buffer.isBuffer(value));
|
|
163
|
+
expect(uuidBuffers).toHaveLength(2);
|
|
164
|
+
expect(uuidBuffers[0]).toHaveLength(16);
|
|
165
|
+
expect(uuidBuffers[1]).toHaveLength(16);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("fills response rest primary key from insertId for autoincrement inserts", async () => {
|
|
169
|
+
const conn = buildWriteConn(1, 42);
|
|
170
|
+
const config = buildAutoIncrementConfig(conn);
|
|
171
|
+
const request: any = {
|
|
172
|
+
name: "Auto Row",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const executor = new SqlExecutor<any>(config, request);
|
|
176
|
+
const result: any = await executor.execute();
|
|
177
|
+
|
|
178
|
+
expect(Array.isArray(result.rest)).toBe(true);
|
|
179
|
+
expect(result.rest).toHaveLength(1);
|
|
180
|
+
expect(result.rest[0]).toMatchObject({
|
|
181
|
+
widget_id: 42,
|
|
182
|
+
name: "Auto Row",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -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;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../types/ormInterfaces";
|
|
15
15
|
import {removeInvalidKeys, removePrefixIfExists, TestRestfulResponse} from "../utils/apiHelpers";
|
|
16
16
|
import {checkCache, evictCacheEntry, setCache, userCustomClearCache} from "../utils/cacheManager";
|
|
17
|
+
import type { SqlAllowListStatus } from "../utils/logSql";
|
|
17
18
|
import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
|
|
18
19
|
import {notifyToast} from "../utils/toastRuntime";
|
|
19
20
|
import {Executor} from "./Executor";
|
|
@@ -81,11 +82,18 @@ export class HttpExecutor<
|
|
|
81
82
|
) {
|
|
82
83
|
type RT = G['RestTableInterface'];
|
|
83
84
|
type PK = G['PrimaryKey'];
|
|
85
|
+
const responseRestRaw = (response.data as any)?.rest;
|
|
86
|
+
const responseRows: Record<string, any>[] = Array.isArray(responseRestRaw)
|
|
87
|
+
? responseRestRaw
|
|
88
|
+
: (responseRestRaw ? [responseRestRaw] : []);
|
|
84
89
|
|
|
85
90
|
if (this.config.restModel.PRIMARY_SHORT.length === 1) {
|
|
86
91
|
const pk = this.config.restModel.PRIMARY_SHORT[0] as PK;
|
|
87
92
|
try {
|
|
88
|
-
|
|
93
|
+
const created = (response.data as any)?.created ?? responseRows[0]?.[pk as string];
|
|
94
|
+
if (created !== undefined) {
|
|
95
|
+
(request as unknown as Record<PK, RT[PK]>)[pk] = created as RT[PK];
|
|
96
|
+
}
|
|
89
97
|
} catch {/* best-effort */}
|
|
90
98
|
} else if (isLocal()) {
|
|
91
99
|
logWithLevel(
|
|
@@ -103,13 +111,13 @@ export class HttpExecutor<
|
|
|
103
111
|
const normalizedRow = this.stripTableNameFromKeys<RT>(row as Partial<RT>);
|
|
104
112
|
return removeInvalidKeys<RT>({
|
|
105
113
|
...normalizedRow,
|
|
106
|
-
...(index
|
|
114
|
+
...(responseRows[index] ?? {}),
|
|
107
115
|
}, this.config.C6.TABLES)
|
|
108
116
|
})
|
|
109
117
|
: [
|
|
110
118
|
removeInvalidKeys<RT>({
|
|
111
119
|
...this.stripTableNameFromKeys<RT>(request as unknown as Partial<RT>),
|
|
112
|
-
...(
|
|
120
|
+
...(responseRows[0] ?? {}),
|
|
113
121
|
}, this.config.C6.TABLES)
|
|
114
122
|
],
|
|
115
123
|
stateKey: this.config.restModel.TABLE_NAME,
|
|
@@ -201,6 +209,7 @@ export class HttpExecutor<
|
|
|
201
209
|
const {
|
|
202
210
|
debug,
|
|
203
211
|
cacheResults = (C6.GET === requestMethod),
|
|
212
|
+
skipReactBootstrap = false,
|
|
204
213
|
dataInsertMultipleRows,
|
|
205
214
|
success,
|
|
206
215
|
fetchDependencies = eFetchDependencies.NONE,
|
|
@@ -253,10 +262,19 @@ export class HttpExecutor<
|
|
|
253
262
|
G['CustomAndRequiredFields'],
|
|
254
263
|
G['RequestTableOverrides']
|
|
255
264
|
>;
|
|
265
|
+
const cacheAllowListStatus: SqlAllowListStatus = this.config.sqlAllowListPath
|
|
266
|
+
? "allowed"
|
|
267
|
+
: "not verified";
|
|
256
268
|
|
|
257
269
|
const evictFromCache =
|
|
258
270
|
requestMethod === GET && cacheResults
|
|
259
|
-
? () => evictCacheEntry(
|
|
271
|
+
? () => evictCacheEntry(
|
|
272
|
+
requestMethod,
|
|
273
|
+
tableName,
|
|
274
|
+
cacheRequestData,
|
|
275
|
+
logContext,
|
|
276
|
+
cacheAllowListStatus,
|
|
277
|
+
)
|
|
260
278
|
: undefined;
|
|
261
279
|
|
|
262
280
|
// literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
|
|
@@ -265,7 +283,13 @@ export class HttpExecutor<
|
|
|
265
283
|
let cachedRequest: Promise<{ data: ResponseDataType }> | false = false;
|
|
266
284
|
|
|
267
285
|
if (cacheResults) {
|
|
268
|
-
cachedRequest = checkCache<ResponseDataType>(
|
|
286
|
+
cachedRequest = checkCache<ResponseDataType>(
|
|
287
|
+
requestMethod,
|
|
288
|
+
tableName,
|
|
289
|
+
cacheRequestData,
|
|
290
|
+
logContext,
|
|
291
|
+
cacheAllowListStatus,
|
|
292
|
+
);
|
|
269
293
|
}
|
|
270
294
|
|
|
271
295
|
if (cachedRequest) {
|
|
@@ -464,6 +488,7 @@ export class HttpExecutor<
|
|
|
464
488
|
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
465
489
|
requestArgumentsSerialized: querySerialized,
|
|
466
490
|
request: axiosActiveRequest,
|
|
491
|
+
allowListStatus: cacheAllowListStatus,
|
|
467
492
|
});
|
|
468
493
|
}
|
|
469
494
|
|
|
@@ -480,6 +505,7 @@ export class HttpExecutor<
|
|
|
480
505
|
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
481
506
|
requestArgumentsSerialized: querySerialized,
|
|
482
507
|
request: axiosActiveRequest,
|
|
508
|
+
allowListStatus: cacheAllowListStatus,
|
|
483
509
|
response,
|
|
484
510
|
final: true,
|
|
485
511
|
});
|
|
@@ -503,6 +529,7 @@ export class HttpExecutor<
|
|
|
503
529
|
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
504
530
|
requestArgumentsSerialized: querySerialized,
|
|
505
531
|
request: axiosActiveRequest,
|
|
532
|
+
allowListStatus: cacheAllowListStatus,
|
|
506
533
|
response,
|
|
507
534
|
});
|
|
508
535
|
}
|
|
@@ -531,7 +558,7 @@ export class HttpExecutor<
|
|
|
531
558
|
response
|
|
532
559
|
});
|
|
533
560
|
|
|
534
|
-
if (undefined !== reactBootstrap && response) {
|
|
561
|
+
if (undefined !== reactBootstrap && response && !skipReactBootstrap) {
|
|
535
562
|
switch (requestMethod) {
|
|
536
563
|
case GET:
|
|
537
564
|
response.data && reactBootstrap.updateRestfulObjectArrays<G['RestTableInterface']>({
|
|
@@ -582,6 +609,7 @@ export class HttpExecutor<
|
|
|
582
609
|
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
583
610
|
requestArgumentsSerialized: querySerialized,
|
|
584
611
|
request: axiosActiveRequest,
|
|
612
|
+
allowListStatus: cacheAllowListStatus,
|
|
585
613
|
response,
|
|
586
614
|
final: !hasNext,
|
|
587
615
|
});
|
|
@@ -870,6 +898,7 @@ export class HttpExecutor<
|
|
|
870
898
|
setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
|
|
871
899
|
requestArgumentsSerialized: querySerialized,
|
|
872
900
|
request: axiosActiveRequest,
|
|
901
|
+
allowListStatus: cacheAllowListStatus,
|
|
873
902
|
response,
|
|
874
903
|
final: true,
|
|
875
904
|
});
|