@carbonorm/carbonnode 3.9.5 → 3.9.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/dist/api/C6Constants.d.ts +4 -0
- package/dist/api/orm/builders/ConditionBuilder.d.ts +14 -2
- package/dist/index.cjs.js +459 -243
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +459 -243
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sqlBuilders.expressions.test.ts +163 -0
- package/src/api/C6Constants.ts +2 -0
- package/src/api/orm/builders/ConditionBuilder.ts +479 -224
- package/src/api/orm/builders/JoinBuilder.ts +8 -5
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { C6C } from '../api/C6Constants';
|
|
3
|
+
import { SelectQueryBuilder } from '../api/orm/queries/SelectQueryBuilder';
|
|
4
|
+
import { buildParcelConfig } from './fixtures/c6.fixture';
|
|
5
|
+
|
|
6
|
+
const Property_Units = {
|
|
7
|
+
TABLE_NAME: 'property_units',
|
|
8
|
+
LOCATION: 'property_units.location',
|
|
9
|
+
PARCEL_ID: 'property_units.parcel_id',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
const Parcel_Sales = {
|
|
13
|
+
TABLE_NAME: 'parcel_sales',
|
|
14
|
+
PARCEL_ID: 'parcel_sales.parcel_id',
|
|
15
|
+
SALE_DATE: 'parcel_sales.sale_date',
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const Parcel_Building_Details = {
|
|
19
|
+
TABLE_NAME: 'parcel_building_details',
|
|
20
|
+
PARCEL_ID: 'parcel_building_details.parcel_id',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
describe('Explicit SQL expression grammar', () => {
|
|
24
|
+
it('supports operator-rooted nested function expressions with explicit OR grouping', () => {
|
|
25
|
+
const config = buildParcelConfig();
|
|
26
|
+
|
|
27
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
28
|
+
[C6C.SELECT]: [Property_Units.PARCEL_ID],
|
|
29
|
+
[C6C.WHERE]: {
|
|
30
|
+
[C6C.OR]: [
|
|
31
|
+
{
|
|
32
|
+
[C6C.LESS_THAN_OR_EQUAL_TO]: [
|
|
33
|
+
{
|
|
34
|
+
[C6C.ST_DISTANCE_SPHERE]: [
|
|
35
|
+
Property_Units.LOCATION,
|
|
36
|
+
{ [C6C.ST_GEOMFROMTEXT]: ['POINT(39.4972468 -105.0403593)', 4326] },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
5000,
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
[C6C.GREATER_THAN]: [
|
|
44
|
+
{
|
|
45
|
+
[C6C.ST_AREA]: [
|
|
46
|
+
{ [C6C.ST_GEOMFROMTEXT]: ['POLYGON((0 0,1 0,1 1,0 1,0 0))', 4326] },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
10000,
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
} as any, false);
|
|
55
|
+
|
|
56
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
57
|
+
|
|
58
|
+
expect(sql).toContain('WHERE');
|
|
59
|
+
expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units.location, ST_GEOMFROMTEXT\('/);
|
|
60
|
+
expect(sql).toMatch(/ST_AREA\(ST_GEOMFROMTEXT\('/);
|
|
61
|
+
expect(sql).toMatch(/<= \?/);
|
|
62
|
+
expect(sql).toMatch(/> \?/);
|
|
63
|
+
expect(sql).toContain('OR');
|
|
64
|
+
expect(params).toEqual([5000, 10000]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('treats safe raw function expressions as SQL expressions', () => {
|
|
68
|
+
const config = buildParcelConfig();
|
|
69
|
+
|
|
70
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
71
|
+
[C6C.SELECT]: [Property_Units.PARCEL_ID],
|
|
72
|
+
[C6C.WHERE]: {
|
|
73
|
+
0: [
|
|
74
|
+
[
|
|
75
|
+
"ST_Distance_Sphere(property_units.location, ST_GeomFromText('POINT(39.4972468 -105.0403593)', 4326))",
|
|
76
|
+
C6C.LESS_THAN_OR_EQUAL_TO,
|
|
77
|
+
5000,
|
|
78
|
+
],
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
} as any, false);
|
|
82
|
+
|
|
83
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
84
|
+
expect(sql).toMatch(
|
|
85
|
+
/ST_Distance_Sphere\(property_units\.location, ST_GeomFromText\('POINT\(39\.4972468 -105\.0403593\)', 4326\)\) <= \?/
|
|
86
|
+
);
|
|
87
|
+
expect(params.slice(-1)[0]).toBe(5000);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects raw function expressions containing unsafe tokens', () => {
|
|
91
|
+
const config = buildParcelConfig();
|
|
92
|
+
|
|
93
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
94
|
+
[C6C.SELECT]: [Property_Units.PARCEL_ID],
|
|
95
|
+
[C6C.WHERE]: {
|
|
96
|
+
0: [
|
|
97
|
+
[
|
|
98
|
+
"ST_Distance_Sphere(property_units.location, ST_GeomFromText('POINT(39.4972468 -105.0403593)', 4326)); DROP TABLE users",
|
|
99
|
+
C6C.LESS_THAN,
|
|
100
|
+
1000,
|
|
101
|
+
],
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
} as any, false);
|
|
105
|
+
|
|
106
|
+
expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Potential SQL injection detected/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('supports explicit AND groupings composed of nested OR clauses', () => {
|
|
110
|
+
const config = buildParcelConfig();
|
|
111
|
+
|
|
112
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
113
|
+
[C6C.SELECT]: [Property_Units.PARCEL_ID],
|
|
114
|
+
[C6C.WHERE]: {
|
|
115
|
+
[C6C.AND]: [
|
|
116
|
+
{ [C6C.GREATER_THAN]: [Property_Units.PARCEL_ID, 100] },
|
|
117
|
+
{
|
|
118
|
+
[C6C.OR]: [
|
|
119
|
+
{ [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, ['2021-01-01', '2022-06-30']] },
|
|
120
|
+
{ [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, ['2023-01-01', '2024-06-30']] },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
} as any, false);
|
|
126
|
+
|
|
127
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
128
|
+
|
|
129
|
+
expect(sql).toMatch(/\(property_units\.parcel_id\) > \?/);
|
|
130
|
+
expect(sql).toMatch(/\(parcel_sales\.sale_date\) BETWEEN \? AND \?/);
|
|
131
|
+
expect(sql).toContain('AND');
|
|
132
|
+
expect(sql).toContain('OR');
|
|
133
|
+
expect(params).toEqual([100, '2021-01-01', '2022-06-30', '2023-01-01', '2024-06-30']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('serializes JOIN clauses deterministically based on declaration order', () => {
|
|
137
|
+
const config = buildParcelConfig();
|
|
138
|
+
|
|
139
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
140
|
+
[C6C.SELECT]: [Property_Units.PARCEL_ID],
|
|
141
|
+
[C6C.JOIN]: {
|
|
142
|
+
[C6C.LEFT]: {
|
|
143
|
+
[Parcel_Sales.TABLE_NAME]: {
|
|
144
|
+
[Parcel_Sales.PARCEL_ID]: Property_Units.PARCEL_ID,
|
|
145
|
+
},
|
|
146
|
+
[Parcel_Building_Details.TABLE_NAME]: {
|
|
147
|
+
[Parcel_Building_Details.PARCEL_ID]: Property_Units.PARCEL_ID,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
[C6C.WHERE]: { [Property_Units.PARCEL_ID]: [C6C.EQUAL, 1] },
|
|
152
|
+
} as any, false);
|
|
153
|
+
|
|
154
|
+
const { sql } = qb.build(Property_Units.TABLE_NAME);
|
|
155
|
+
|
|
156
|
+
const firstJoin = sql.indexOf('JOIN `parcel_sales`');
|
|
157
|
+
const secondJoin = sql.indexOf('JOIN `parcel_building_details`');
|
|
158
|
+
|
|
159
|
+
expect(firstJoin).toBeGreaterThan(-1);
|
|
160
|
+
expect(secondJoin).toBeGreaterThan(firstJoin);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
package/src/api/C6Constants.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const C6Constants = {
|
|
|
6
6
|
ADDTIME: 'ADDTIME',
|
|
7
7
|
AS: 'AS',
|
|
8
8
|
ASC: 'ASC',
|
|
9
|
+
AND: 'AND',
|
|
9
10
|
|
|
10
11
|
BETWEEN: 'BETWEEN',
|
|
11
12
|
|
|
@@ -92,6 +93,7 @@ export const C6Constants = {
|
|
|
92
93
|
NULL: 'NULL',
|
|
93
94
|
|
|
94
95
|
ORDER: 'ORDER',
|
|
96
|
+
OR: 'OR',
|
|
95
97
|
|
|
96
98
|
PAGE: 'PAGE',
|
|
97
99
|
PAGINATION: 'PAGINATION',
|