@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
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
export interface SqlBuilderResult {
|
|
5
2
|
sql: string;
|
|
6
3
|
params: any[] | { [key: string]: any }; // params can be an array or an object for named placeholders
|
|
@@ -16,9 +13,180 @@ export function convertHexIfBinary(
|
|
|
16
13
|
typeof val === 'string' &&
|
|
17
14
|
/^[0-9a-fA-F]{32}$/.test(val) &&
|
|
18
15
|
typeof columnDef === 'object' &&
|
|
19
|
-
columnDef.MYSQL_TYPE.toUpperCase().includes('BINARY')
|
|
16
|
+
String(columnDef.MYSQL_TYPE ?? '').toUpperCase().includes('BINARY')
|
|
20
17
|
) {
|
|
21
18
|
return Buffer.from(val, 'hex');
|
|
22
19
|
}
|
|
23
20
|
return val;
|
|
24
21
|
}
|
|
22
|
+
|
|
23
|
+
type TemporalMysqlType = 'date' | 'datetime' | 'timestamp' | 'time' | 'year';
|
|
24
|
+
|
|
25
|
+
const TEMPORAL_TYPES = new Set<TemporalMysqlType>([
|
|
26
|
+
'date',
|
|
27
|
+
'datetime',
|
|
28
|
+
'timestamp',
|
|
29
|
+
'time',
|
|
30
|
+
'year',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const MYSQL_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
34
|
+
const MYSQL_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?$/;
|
|
35
|
+
const MYSQL_TIME_REGEX = /^-?\d{2,3}:\d{2}:\d{2}(?:\.\d{1,6})?$/;
|
|
36
|
+
const ISO_DATETIME_REGEX = /^(\d{4}-\d{2}-\d{2})[Tt](\d{2}:\d{2}:\d{2})(\.\d{1,6})?([zZ]|[+-]\d{2}:\d{2})?$/;
|
|
37
|
+
|
|
38
|
+
const pad2 = (value: number): string => value.toString().padStart(2, '0');
|
|
39
|
+
|
|
40
|
+
function trimFraction(value: string, precision: number): string {
|
|
41
|
+
const [base, fractionRaw] = value.split('.', 2);
|
|
42
|
+
if (precision <= 0 || !fractionRaw) return base;
|
|
43
|
+
return `${base}.${fractionRaw.slice(0, precision).padEnd(precision, '0')}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeFraction(raw: string | undefined, precision: number): string {
|
|
47
|
+
if (precision <= 0) return '';
|
|
48
|
+
if (!raw) return '';
|
|
49
|
+
const digits = raw.startsWith('.') ? raw.slice(1) : raw;
|
|
50
|
+
return `.${digits.slice(0, precision).padEnd(precision, '0')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatDateUtc(value: Date): string {
|
|
54
|
+
return `${value.getUTCFullYear()}-${pad2(value.getUTCMonth() + 1)}-${pad2(value.getUTCDate())}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatTimeUtc(value: Date, precision: number): string {
|
|
58
|
+
const base = `${pad2(value.getUTCHours())}:${pad2(value.getUTCMinutes())}:${pad2(value.getUTCSeconds())}`;
|
|
59
|
+
if (precision <= 0) return base;
|
|
60
|
+
|
|
61
|
+
const millis = value.getUTCMilliseconds().toString().padStart(3, '0');
|
|
62
|
+
const fraction = millis.slice(0, Math.min(precision, 3)).padEnd(precision, '0');
|
|
63
|
+
return `${base}.${fraction}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatDateTimeUtc(value: Date, precision: number): string {
|
|
67
|
+
return `${formatDateUtc(value)} ${formatTimeUtc(value, precision)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseEpochNumber(value: number): Date | undefined {
|
|
71
|
+
if (!Number.isFinite(value)) return undefined;
|
|
72
|
+
const abs = Math.abs(value);
|
|
73
|
+
if (abs >= 1e12) {
|
|
74
|
+
const date = new Date(value);
|
|
75
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
76
|
+
}
|
|
77
|
+
if (abs >= 1e9) {
|
|
78
|
+
const date = new Date(value * 1000);
|
|
79
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseTemporalType(columnDef?: any): { baseType?: TemporalMysqlType; precision: number } {
|
|
85
|
+
const raw = String(columnDef?.MYSQL_TYPE ?? '').trim().toLowerCase();
|
|
86
|
+
if (!raw) return { baseType: undefined, precision: 0 };
|
|
87
|
+
|
|
88
|
+
const base = raw.split(/[\s(]/, 1)[0] as TemporalMysqlType;
|
|
89
|
+
if (!TEMPORAL_TYPES.has(base)) return { baseType: undefined, precision: 0 };
|
|
90
|
+
|
|
91
|
+
const precisionMatch = raw.match(/^(?:datetime|timestamp|time)\((\d+)\)/);
|
|
92
|
+
if (!precisionMatch) return { baseType: base, precision: 0 };
|
|
93
|
+
const parsed = Number.parseInt(precisionMatch[1], 10);
|
|
94
|
+
if (!Number.isFinite(parsed)) return { baseType: base, precision: 0 };
|
|
95
|
+
return { baseType: base, precision: Math.max(0, Math.min(6, parsed)) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeTemporalString(
|
|
99
|
+
value: string,
|
|
100
|
+
baseType: TemporalMysqlType,
|
|
101
|
+
precision: number,
|
|
102
|
+
): string {
|
|
103
|
+
const trimmed = value.trim();
|
|
104
|
+
if (!trimmed) return value;
|
|
105
|
+
|
|
106
|
+
if (baseType === 'date') {
|
|
107
|
+
if (MYSQL_DATE_REGEX.test(trimmed)) return trimmed;
|
|
108
|
+
const iso = trimmed.match(ISO_DATETIME_REGEX);
|
|
109
|
+
if (iso) {
|
|
110
|
+
const [, datePart, , , timezonePart] = iso;
|
|
111
|
+
if (!timezonePart) return datePart;
|
|
112
|
+
const parsed = new Date(trimmed);
|
|
113
|
+
return Number.isNaN(parsed.getTime()) ? value : formatDateUtc(parsed);
|
|
114
|
+
}
|
|
115
|
+
const parsed = new Date(trimmed);
|
|
116
|
+
return Number.isNaN(parsed.getTime()) ? value : formatDateUtc(parsed);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (baseType === 'time') {
|
|
120
|
+
if (MYSQL_TIME_REGEX.test(trimmed)) return trimFraction(trimmed, precision);
|
|
121
|
+
const iso = trimmed.match(ISO_DATETIME_REGEX);
|
|
122
|
+
if (iso) {
|
|
123
|
+
const [, , timePart, fractionPart, timezonePart] = iso;
|
|
124
|
+
if (!timezonePart) {
|
|
125
|
+
return `${timePart}${normalizeFraction(fractionPart, precision)}`;
|
|
126
|
+
}
|
|
127
|
+
const parsed = new Date(trimmed);
|
|
128
|
+
return Number.isNaN(parsed.getTime()) ? value : formatTimeUtc(parsed, precision);
|
|
129
|
+
}
|
|
130
|
+
const parsed = new Date(trimmed);
|
|
131
|
+
return Number.isNaN(parsed.getTime()) ? value : formatTimeUtc(parsed, precision);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (baseType === 'year') {
|
|
135
|
+
if (/^\d{2,4}$/.test(trimmed)) return trimmed;
|
|
136
|
+
const parsed = new Date(trimmed);
|
|
137
|
+
return Number.isNaN(parsed.getTime()) ? value : String(parsed.getUTCFullYear());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (MYSQL_DATETIME_REGEX.test(trimmed)) return trimFraction(trimmed, precision);
|
|
141
|
+
const iso = trimmed.match(ISO_DATETIME_REGEX);
|
|
142
|
+
if (iso) {
|
|
143
|
+
const [, datePart, timePart, fractionPart, timezonePart] = iso;
|
|
144
|
+
if (!timezonePart) {
|
|
145
|
+
return `${datePart} ${timePart}${normalizeFraction(fractionPart, precision)}`;
|
|
146
|
+
}
|
|
147
|
+
const parsed = new Date(trimmed);
|
|
148
|
+
return Number.isNaN(parsed.getTime()) ? value : formatDateTimeUtc(parsed, precision);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const parsed = new Date(trimmed);
|
|
152
|
+
return Number.isNaN(parsed.getTime()) ? value : formatDateTimeUtc(parsed, precision);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function convertTemporalIfNeeded(value: any, columnDef?: any): any {
|
|
156
|
+
const { baseType, precision } = parseTemporalType(columnDef);
|
|
157
|
+
if (!baseType) return value;
|
|
158
|
+
|
|
159
|
+
if (value === null || value === undefined) return value;
|
|
160
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(value)) return value;
|
|
161
|
+
|
|
162
|
+
if (value instanceof Date) {
|
|
163
|
+
if (baseType === 'date') return formatDateUtc(value);
|
|
164
|
+
if (baseType === 'time') return formatTimeUtc(value, precision);
|
|
165
|
+
if (baseType === 'year') return String(value.getUTCFullYear());
|
|
166
|
+
return formatDateTimeUtc(value, precision);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof value === 'number') {
|
|
170
|
+
const parsed = parseEpochNumber(value);
|
|
171
|
+
if (!parsed) return value;
|
|
172
|
+
if (baseType === 'date') return formatDateUtc(parsed);
|
|
173
|
+
if (baseType === 'time') return formatTimeUtc(parsed, precision);
|
|
174
|
+
if (baseType === 'year') return String(parsed.getUTCFullYear());
|
|
175
|
+
return formatDateTimeUtc(parsed, precision);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof value === 'string') {
|
|
179
|
+
return normalizeTemporalString(value, baseType, precision);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function convertSqlValueForColumn(
|
|
186
|
+
col: string,
|
|
187
|
+
val: any,
|
|
188
|
+
columnDef?: any
|
|
189
|
+
): any {
|
|
190
|
+
const binaryConverted = convertHexIfBinary(col, val, columnDef);
|
|
191
|
+
return convertTemporalIfNeeded(binaryConverted, columnDef);
|
|
192
|
+
}
|
package/src/types/mysqlTypes.ts
CHANGED
|
@@ -1,15 +1,119 @@
|
|
|
1
|
-
|
|
2
1
|
// ========================
|
|
3
|
-
//
|
|
2
|
+
// SQL Operators & Expressions
|
|
4
3
|
// ========================
|
|
5
4
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
export const SQL_KNOWN_FUNCTIONS = [
|
|
6
|
+
'ADDDATE',
|
|
7
|
+
'ADDTIME',
|
|
8
|
+
'CONCAT',
|
|
9
|
+
'CONVERT_TZ',
|
|
10
|
+
'COUNT',
|
|
11
|
+
'COUNT_ALL',
|
|
12
|
+
'CURRENT_DATE',
|
|
13
|
+
'CURRENT_TIMESTAMP',
|
|
14
|
+
'DAY',
|
|
15
|
+
'DAY_HOUR',
|
|
16
|
+
'DAY_MICROSECOND',
|
|
17
|
+
'DAY_MINUTE',
|
|
18
|
+
'DAY_SECOND',
|
|
19
|
+
'DAYNAME',
|
|
20
|
+
'DAYOFMONTH',
|
|
21
|
+
'DAYOFWEEK',
|
|
22
|
+
'DAYOFYEAR',
|
|
23
|
+
'DATE',
|
|
24
|
+
'DATE_ADD',
|
|
25
|
+
'DATEDIFF',
|
|
26
|
+
'DATE_SUB',
|
|
27
|
+
'DATE_FORMAT',
|
|
28
|
+
'EXTRACT',
|
|
29
|
+
'FROM_DAYS',
|
|
30
|
+
'FROM_UNIXTIME',
|
|
31
|
+
'GET_FORMAT',
|
|
32
|
+
'GROUP_CONCAT',
|
|
33
|
+
'HEX',
|
|
34
|
+
'HOUR',
|
|
35
|
+
'HOUR_MICROSECOND',
|
|
36
|
+
'HOUR_MINUTE',
|
|
37
|
+
'HOUR_SECOND',
|
|
38
|
+
'INTERVAL',
|
|
39
|
+
'LOCALTIME',
|
|
40
|
+
'LOCALTIMESTAMP',
|
|
41
|
+
'MAKEDATE',
|
|
42
|
+
'MAKETIME',
|
|
43
|
+
'MAX',
|
|
44
|
+
'MBRContains',
|
|
45
|
+
'MICROSECOND',
|
|
46
|
+
'MIN',
|
|
47
|
+
'MINUTE',
|
|
48
|
+
'MINUTE_MICROSECOND',
|
|
49
|
+
'MINUTE_SECOND',
|
|
50
|
+
'MONTH',
|
|
51
|
+
'MONTHNAME',
|
|
52
|
+
'NOW',
|
|
53
|
+
'POINT',
|
|
54
|
+
'POLYGON',
|
|
55
|
+
'SECOND',
|
|
56
|
+
'SECOND_MICROSECOND',
|
|
57
|
+
'ST_Area',
|
|
58
|
+
'ST_AsBinary',
|
|
59
|
+
'ST_AsText',
|
|
60
|
+
'ST_Buffer',
|
|
61
|
+
'ST_Contains',
|
|
62
|
+
'ST_Crosses',
|
|
63
|
+
'ST_Difference',
|
|
64
|
+
'ST_Dimension',
|
|
65
|
+
'ST_Disjoint',
|
|
66
|
+
'ST_Distance',
|
|
67
|
+
'ST_Distance_Sphere',
|
|
68
|
+
'ST_EndPoint',
|
|
69
|
+
'ST_Envelope',
|
|
70
|
+
'ST_Equals',
|
|
71
|
+
'ST_GeomFromGeoJSON',
|
|
72
|
+
'ST_GeomFromText',
|
|
73
|
+
'ST_GeomFromWKB',
|
|
74
|
+
'ST_Intersects',
|
|
75
|
+
'ST_Length',
|
|
76
|
+
'ST_MakeEnvelope',
|
|
77
|
+
'ST_Overlaps',
|
|
78
|
+
'ST_Point',
|
|
79
|
+
'ST_SetSRID',
|
|
80
|
+
'ST_SRID',
|
|
81
|
+
'ST_StartPoint',
|
|
82
|
+
'ST_SymDifference',
|
|
83
|
+
'ST_Touches',
|
|
84
|
+
'ST_Union',
|
|
85
|
+
'ST_Within',
|
|
86
|
+
'ST_X',
|
|
87
|
+
'ST_Y',
|
|
88
|
+
'STR_TO_DATE',
|
|
89
|
+
'SUBDATE',
|
|
90
|
+
'SUBTIME',
|
|
91
|
+
'SUM',
|
|
92
|
+
'SYSDATE',
|
|
93
|
+
'TIME',
|
|
94
|
+
'TIME_FORMAT',
|
|
95
|
+
'TIME_TO_SEC',
|
|
96
|
+
'TIMEDIFF',
|
|
97
|
+
'TIMESTAMP',
|
|
98
|
+
'TIMESTAMPADD',
|
|
99
|
+
'TIMESTAMPDIFF',
|
|
100
|
+
'TO_DAYS',
|
|
101
|
+
'TO_SECONDS',
|
|
102
|
+
'TRANSACTION_TIMESTAMP',
|
|
103
|
+
'UNHEX',
|
|
104
|
+
'UNIX_TIMESTAMP',
|
|
105
|
+
'UTC_DATE',
|
|
106
|
+
'UTC_TIME',
|
|
107
|
+
'UTC_TIMESTAMP',
|
|
108
|
+
'WEEKDAY',
|
|
109
|
+
'WEEKOFYEAR',
|
|
110
|
+
'YEARWEEK',
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
export type SQLKnownFunction = typeof SQL_KNOWN_FUNCTIONS[number];
|
|
114
|
+
|
|
115
|
+
// Backwards alias for existing public import name.
|
|
116
|
+
export type SQLFunction = SQLKnownFunction;
|
|
13
117
|
|
|
14
118
|
export type SQLComparisonOperator =
|
|
15
119
|
| '='
|
|
@@ -31,3 +135,20 @@ export type JoinType = 'INNER' | 'LEFT_OUTER' | 'RIGHT_OUTER';
|
|
|
31
135
|
|
|
32
136
|
export type OrderDirection = 'ASC' | 'DESC';
|
|
33
137
|
|
|
138
|
+
export type SQLExpression =
|
|
139
|
+
| string
|
|
140
|
+
| number
|
|
141
|
+
| boolean
|
|
142
|
+
| null
|
|
143
|
+
| SQLExpressionTuple;
|
|
144
|
+
|
|
145
|
+
export type SQLExpressionTuple =
|
|
146
|
+
| ['AS', SQLExpression, string]
|
|
147
|
+
| ['DISTINCT', SQLExpression]
|
|
148
|
+
| ['CALL', string, ...SQLExpression[]]
|
|
149
|
+
| ['LIT', any]
|
|
150
|
+
| ['PARAM', any]
|
|
151
|
+
| ['SUBSELECT', Record<string, any>]
|
|
152
|
+
| [SQLKnownFunction, ...SQLExpression[]];
|
|
153
|
+
|
|
154
|
+
export type OrderTerm = [SQLExpression, OrderDirection?];
|
|
@@ -2,7 +2,7 @@ import type {AxiosInstance, AxiosResponse} from "axios";
|
|
|
2
2
|
import type {Pool} from "mysql2/promise";
|
|
3
3
|
import {eFetchDependencies} from "./dynamicFetching";
|
|
4
4
|
import {Modify} from "./modifyTypes";
|
|
5
|
-
import {JoinType,
|
|
5
|
+
import {JoinType, OrderTerm, SQLComparisonOperator, SQLExpression} from "./mysqlTypes";
|
|
6
6
|
import type {CarbonReact, iStateAdapter} from "@carbonorm/carbonreact";
|
|
7
7
|
import type {OrmGenerics} from "./ormGenerics";
|
|
8
8
|
import {restOrm} from "../api/restOrm";
|
|
@@ -49,9 +49,7 @@ export type SubSelect<T extends { [key: string]: any } = any> = {
|
|
|
49
49
|
|
|
50
50
|
export type SelectField<T extends { [key: string]: any } = any> =
|
|
51
51
|
| keyof T
|
|
52
|
-
|
|
|
53
|
-
| [SQLFunction, keyof T]
|
|
54
|
-
| [SQLFunction, keyof T, string]
|
|
52
|
+
| SQLExpression
|
|
55
53
|
| SubSelect<T>;
|
|
56
54
|
|
|
57
55
|
export type WhereClause<T = any> = Partial<T> | LogicalGroup<T> | ComparisonClause<T>;
|
|
@@ -62,10 +60,10 @@ export type JoinTableCondition<T = any> = Partial<T> | WhereClause<T>[] | Compar
|
|
|
62
60
|
export type JoinClause<T = any> = { [table: string]: JoinTableCondition<T>; };
|
|
63
61
|
export type Join<T = any> = { [K in JoinType]?: JoinClause<T>; };
|
|
64
62
|
|
|
65
|
-
export type Pagination
|
|
63
|
+
export type Pagination = {
|
|
66
64
|
PAGE?: number;
|
|
67
65
|
LIMIT?: number | null;
|
|
68
|
-
ORDER?:
|
|
66
|
+
ORDER?: OrderTerm[];
|
|
69
67
|
};
|
|
70
68
|
|
|
71
69
|
export type RequestGetPutDeleteBody<T extends { [key: string]: any } = any> = T | {
|
|
@@ -74,7 +72,7 @@ export type RequestGetPutDeleteBody<T extends { [key: string]: any } = any> = T
|
|
|
74
72
|
DELETE?: boolean;
|
|
75
73
|
WHERE?: WhereClause<T>;
|
|
76
74
|
JOIN?: Join<T>;
|
|
77
|
-
PAGINATION?: Pagination
|
|
75
|
+
PAGINATION?: Pagination;
|
|
78
76
|
};
|
|
79
77
|
|
|
80
78
|
export type RequestPostBody<T extends { [key: string]: any } = any> = T | {
|
|
@@ -85,6 +83,7 @@ export type RequestPostBody<T extends { [key: string]: any } = any> = T | {
|
|
|
85
83
|
export type iAPI<T extends { [key: string]: any }> = T & {
|
|
86
84
|
dataInsertMultipleRows?: T[];
|
|
87
85
|
cacheResults?: boolean;
|
|
86
|
+
skipReactBootstrap?: boolean;
|
|
88
87
|
fetchDependencies?: number | eFetchDependencies | Awaited<iGetC6RestResponse<any>>[];
|
|
89
88
|
debug?: boolean;
|
|
90
89
|
success?: string | ((r: AxiosResponse) => string | void);
|
|
@@ -236,6 +235,7 @@ export interface iRest<
|
|
|
236
235
|
logLevel?: number;
|
|
237
236
|
verbose?: boolean;
|
|
238
237
|
sqlAllowListPath?: string;
|
|
238
|
+
sqlQueryNormalizer?: (sql: string) => string;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
export interface iConstraint {
|
|
@@ -19,7 +19,7 @@ export function normalizeSingularRequest<
|
|
|
19
19
|
): RequestQueryBody<Method, T, Custom, Overrides> {
|
|
20
20
|
if (request == null || typeof request !== 'object') return request;
|
|
21
21
|
|
|
22
|
-
const specialKeys = new Set([
|
|
22
|
+
const specialKeys: Set<string> = new Set([
|
|
23
23
|
C6C.SELECT,
|
|
24
24
|
C6C.UPDATE,
|
|
25
25
|
C6C.DELETE,
|
|
@@ -88,6 +88,7 @@ export function normalizeSingularRequest<
|
|
|
88
88
|
const {
|
|
89
89
|
dataInsertMultipleRows,
|
|
90
90
|
cacheResults,
|
|
91
|
+
skipReactBootstrap,
|
|
91
92
|
fetchDependencies,
|
|
92
93
|
debug,
|
|
93
94
|
success,
|
|
@@ -103,10 +104,13 @@ export function normalizeSingularRequest<
|
|
|
103
104
|
const pkFullValues = Object.fromEntries(
|
|
104
105
|
Object.entries(pkValues).map(([k, v]) => [shortToFull[k] ?? k, v])
|
|
105
106
|
);
|
|
107
|
+
const pkWhereExpressions = Object.fromEntries(
|
|
108
|
+
Object.entries(pkFullValues).map(([column, value]) => [column, [C6C.EQUAL, [C6C.LIT, value]]]),
|
|
109
|
+
);
|
|
106
110
|
|
|
107
111
|
if (requestMethod === C6C.GET) {
|
|
108
112
|
const normalized: any = {
|
|
109
|
-
WHERE: { ...
|
|
113
|
+
WHERE: { ...pkWhereExpressions },
|
|
110
114
|
};
|
|
111
115
|
// Preserve pagination if any was added previously
|
|
112
116
|
if ((request as any)[C6C.PAGINATION]) {
|
|
@@ -116,6 +120,7 @@ export function normalizeSingularRequest<
|
|
|
116
120
|
...normalized,
|
|
117
121
|
dataInsertMultipleRows,
|
|
118
122
|
cacheResults,
|
|
123
|
+
skipReactBootstrap,
|
|
119
124
|
fetchDependencies,
|
|
120
125
|
debug,
|
|
121
126
|
success,
|
|
@@ -126,12 +131,13 @@ export function normalizeSingularRequest<
|
|
|
126
131
|
if (requestMethod === C6C.DELETE) {
|
|
127
132
|
const normalized: any = {
|
|
128
133
|
[C6C.DELETE]: true,
|
|
129
|
-
WHERE: { ...
|
|
134
|
+
WHERE: { ...pkWhereExpressions },
|
|
130
135
|
};
|
|
131
136
|
return {
|
|
132
137
|
...normalized,
|
|
133
138
|
dataInsertMultipleRows,
|
|
134
139
|
cacheResults,
|
|
140
|
+
skipReactBootstrap,
|
|
135
141
|
fetchDependencies,
|
|
136
142
|
debug,
|
|
137
143
|
success,
|
|
@@ -154,13 +160,14 @@ export function normalizeSingularRequest<
|
|
|
154
160
|
|
|
155
161
|
const normalized: any = {
|
|
156
162
|
[C6C.UPDATE]: updateBody,
|
|
157
|
-
WHERE: { ...
|
|
163
|
+
WHERE: { ...pkWhereExpressions },
|
|
158
164
|
};
|
|
159
165
|
|
|
160
166
|
return {
|
|
161
167
|
...normalized,
|
|
162
168
|
dataInsertMultipleRows,
|
|
163
169
|
cacheResults,
|
|
170
|
+
skipReactBootstrap,
|
|
164
171
|
fetchDependencies,
|
|
165
172
|
debug,
|
|
166
173
|
success,
|
|
@@ -6,7 +6,12 @@ type AllowListCacheEntry = {
|
|
|
6
6
|
size: number;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
export type SqlQueryNormalizer = (sql: string) => string;
|
|
10
|
+
|
|
11
|
+
const DEFAULT_NORMALIZER_CACHE_KEY = "__default__";
|
|
12
|
+
type AllowListCacheKey = SqlQueryNormalizer | typeof DEFAULT_NORMALIZER_CACHE_KEY;
|
|
13
|
+
|
|
14
|
+
const allowListCache = new Map<string, Map<AllowListCacheKey, AllowListCacheEntry>>();
|
|
10
15
|
|
|
11
16
|
const ANSI_ESCAPE_REGEX = /\x1b\[[0-9;]*m/g;
|
|
12
17
|
const COLLAPSED_BIND_ROW_REGEX = /\(\?\s*×\d+\)/g;
|
|
@@ -91,7 +96,26 @@ export const normalizeSql = (sql: string): string => {
|
|
|
91
96
|
return normalized.replace(/\s+/g, " ").trim();
|
|
92
97
|
};
|
|
93
98
|
|
|
94
|
-
const
|
|
99
|
+
export const normalizeSqlWith = (
|
|
100
|
+
sql: string,
|
|
101
|
+
sqlQueryNormalizer?: SqlQueryNormalizer,
|
|
102
|
+
): string => {
|
|
103
|
+
const normalized = normalizeSql(sql);
|
|
104
|
+
if (!sqlQueryNormalizer) return normalized;
|
|
105
|
+
|
|
106
|
+
const customized = sqlQueryNormalizer(normalized);
|
|
107
|
+
if (typeof customized !== "string") {
|
|
108
|
+
throw new Error("sqlQueryNormalizer must return a string.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return customized.replace(/\s+/g, " ").trim();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const parseAllowList = (
|
|
115
|
+
raw: string,
|
|
116
|
+
sourcePath: string,
|
|
117
|
+
sqlQueryNormalizer?: SqlQueryNormalizer,
|
|
118
|
+
): string[] => {
|
|
95
119
|
let parsed: unknown;
|
|
96
120
|
try {
|
|
97
121
|
parsed = JSON.parse(raw);
|
|
@@ -105,7 +129,7 @@ const parseAllowList = (raw: string, sourcePath: string): string[] => {
|
|
|
105
129
|
|
|
106
130
|
const sqlEntries = parsed
|
|
107
131
|
.filter((entry): entry is string => typeof entry === "string")
|
|
108
|
-
.map(
|
|
132
|
+
.map((entry) => normalizeSqlWith(entry, sqlQueryNormalizer))
|
|
109
133
|
.filter((entry) => entry.length > 0);
|
|
110
134
|
|
|
111
135
|
if (sqlEntries.length !== parsed.length) {
|
|
@@ -115,7 +139,10 @@ const parseAllowList = (raw: string, sourcePath: string): string[] => {
|
|
|
115
139
|
return sqlEntries;
|
|
116
140
|
};
|
|
117
141
|
|
|
118
|
-
export const loadSqlAllowList = async (
|
|
142
|
+
export const loadSqlAllowList = async (
|
|
143
|
+
allowListPath: string,
|
|
144
|
+
sqlQueryNormalizer?: SqlQueryNormalizer,
|
|
145
|
+
): Promise<Set<string>> => {
|
|
119
146
|
if (!isNode()) {
|
|
120
147
|
throw new Error("SQL allowlist validation requires a Node runtime.");
|
|
121
148
|
}
|
|
@@ -129,7 +156,10 @@ export const loadSqlAllowList = async (allowListPath: string): Promise<Set<strin
|
|
|
129
156
|
throw new Error(`SQL allowlist file not found at ${allowListPath}.`);
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
const
|
|
159
|
+
const pathCache = allowListCache.get(allowListPath)
|
|
160
|
+
?? new Map<AllowListCacheKey, AllowListCacheEntry>();
|
|
161
|
+
const cacheKey: AllowListCacheKey = sqlQueryNormalizer ?? DEFAULT_NORMALIZER_CACHE_KEY;
|
|
162
|
+
const cached = pathCache.get(cacheKey);
|
|
133
163
|
if (
|
|
134
164
|
cached &&
|
|
135
165
|
cached.mtimeMs === fileStat.mtimeMs &&
|
|
@@ -145,13 +175,14 @@ export const loadSqlAllowList = async (allowListPath: string): Promise<Set<strin
|
|
|
145
175
|
throw new Error(`SQL allowlist file not found at ${allowListPath}.`);
|
|
146
176
|
}
|
|
147
177
|
|
|
148
|
-
const sqlEntries = parseAllowList(raw, allowListPath);
|
|
178
|
+
const sqlEntries = parseAllowList(raw, allowListPath, sqlQueryNormalizer);
|
|
149
179
|
const allowList = new Set(sqlEntries);
|
|
150
|
-
|
|
180
|
+
pathCache.set(cacheKey, {
|
|
151
181
|
allowList,
|
|
152
182
|
mtimeMs: fileStat.mtimeMs,
|
|
153
183
|
size: fileStat.size,
|
|
154
184
|
});
|
|
185
|
+
allowListCache.set(allowListPath, pathCache);
|
|
155
186
|
return allowList;
|
|
156
187
|
};
|
|
157
188
|
|
|
@@ -186,10 +217,11 @@ export const extractSqlEntries = (payload: unknown): string[] => {
|
|
|
186
217
|
|
|
187
218
|
export const collectSqlAllowListEntries = (
|
|
188
219
|
payload: unknown,
|
|
189
|
-
entries: Set<string> = new Set<string>()
|
|
220
|
+
entries: Set<string> = new Set<string>(),
|
|
221
|
+
sqlQueryNormalizer?: SqlQueryNormalizer,
|
|
190
222
|
): Set<string> => {
|
|
191
223
|
const sqlEntries = extractSqlEntries(payload)
|
|
192
|
-
.map(
|
|
224
|
+
.map((entry) => normalizeSqlWith(entry, sqlQueryNormalizer))
|
|
193
225
|
.filter((entry) => entry.length > 0);
|
|
194
226
|
|
|
195
227
|
sqlEntries.forEach((entry) => entries.add(entry));
|
|
@@ -199,7 +231,8 @@ export const collectSqlAllowListEntries = (
|
|
|
199
231
|
|
|
200
232
|
export const compileSqlAllowList = async (
|
|
201
233
|
allowListPath: string,
|
|
202
|
-
entries: Iterable<string
|
|
234
|
+
entries: Iterable<string>,
|
|
235
|
+
sqlQueryNormalizer?: SqlQueryNormalizer,
|
|
203
236
|
): Promise<string[]> => {
|
|
204
237
|
if (!isNode()) {
|
|
205
238
|
throw new Error("SQL allowlist compilation requires a Node runtime.");
|
|
@@ -212,7 +245,7 @@ export const compileSqlAllowList = async (
|
|
|
212
245
|
|
|
213
246
|
const compiled = Array.from(new Set(
|
|
214
247
|
Array.from(entries)
|
|
215
|
-
.map(
|
|
248
|
+
.map((entry) => normalizeSqlWith(entry, sqlQueryNormalizer))
|
|
216
249
|
.filter((entry) => entry.length > 0)
|
|
217
250
|
)).sort();
|
|
218
251
|
|