@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.
Files changed (90) hide show
  1. package/README.md +521 -259
  2. package/dist/constants/C6Constants.d.ts +342 -338
  3. package/dist/executors/SqlExecutor.d.ts +1 -0
  4. package/dist/index.cjs.js +746 -290
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.esm.js +737 -291
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/orm/builders/AggregateBuilder.d.ts +5 -1
  10. package/dist/orm/builders/ConditionBuilder.d.ts +2 -3
  11. package/dist/orm/builders/ExpressionSerializer.d.ts +22 -0
  12. package/dist/orm/builders/PaginationBuilder.d.ts +4 -6
  13. package/dist/orm/queryHelpers.d.ts +12 -1
  14. package/dist/orm/utils/sqlUtils.d.ts +1 -0
  15. package/dist/types/mysqlTypes.d.ts +6 -1
  16. package/dist/types/ormInterfaces.d.ts +7 -5
  17. package/dist/utils/sqlAllowList.d.ts +5 -3
  18. package/package.json +2 -2
  19. package/scripts/assets/handlebars/C6.test.ts.handlebars +4 -4
  20. package/src/__tests__/expressServer.e2e.test.ts +26 -17
  21. package/src/__tests__/fixtures/c6.fixture.ts +33 -0
  22. package/src/__tests__/httpExecutorSingular.e2e.test.ts +53 -14
  23. package/src/__tests__/normalizeSingularRequest.test.ts +26 -8
  24. package/src/__tests__/sakila-db/C6.js +1 -1
  25. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  26. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  27. package/src/__tests__/sakila-db/C6.sqlAllowList.json +1 -1
  28. package/src/__tests__/sakila-db/C6.test.ts +4 -4
  29. package/src/__tests__/sakila-db/C6.ts +1 -1
  30. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +6 -6
  31. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  32. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  33. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  34. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +19 -12
  35. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  36. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  37. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  38. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +4 -4
  39. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  40. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  41. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  42. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +4 -4
  43. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  44. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  45. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  46. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +4 -4
  47. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  48. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  49. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  50. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +10 -10
  51. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  52. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  53. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  54. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +4 -4
  55. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  56. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  57. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  58. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +2 -2
  59. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  60. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  61. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  62. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +4 -4
  63. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  64. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  65. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  66. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +4 -4
  67. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  68. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  69. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +6 -6
  70. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
  71. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
  72. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
  73. package/src/__tests__/sqlAllowList.test.ts +56 -1
  74. package/src/__tests__/sqlBuilders.complex.test.ts +62 -74
  75. package/src/__tests__/sqlBuilders.expressions.test.ts +58 -30
  76. package/src/__tests__/sqlBuilders.test.ts +106 -5
  77. package/src/constants/C6Constants.ts +3 -1
  78. package/src/executors/HttpExecutor.ts +2 -1
  79. package/src/executors/SqlExecutor.ts +29 -4
  80. package/src/index.ts +1 -0
  81. package/src/orm/builders/AggregateBuilder.ts +67 -106
  82. package/src/orm/builders/ConditionBuilder.ts +72 -103
  83. package/src/orm/builders/ExpressionSerializer.ts +275 -0
  84. package/src/orm/builders/PaginationBuilder.ts +24 -34
  85. package/src/orm/queryHelpers.ts +29 -0
  86. package/src/orm/utils/sqlUtils.ts +172 -4
  87. package/src/types/mysqlTypes.ts +130 -9
  88. package/src/types/ormInterfaces.ts +7 -7
  89. package/src/utils/normalizeSingularRequest.ts +11 -4
  90. 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
+ }
@@ -1,15 +1,119 @@
1
-
2
1
  // ========================
3
- // 🔧 SQL Operator & Helpers
2
+ // SQL Operators & Expressions
4
3
  // ========================
5
4
 
6
- export type SQLFunction =
7
- | 'COUNT'
8
- | 'GROUP_CONCAT'
9
- | 'MAX'
10
- | 'MIN'
11
- | 'SUM'
12
- | 'DISTINCT';
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, OrderDirection, SQLComparisonOperator, SQLFunction} from "./mysqlTypes";
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
- | [keyof T, 'AS', string]
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<T = any> = {
63
+ export type Pagination = {
66
64
  PAGE?: number;
67
65
  LIMIT?: number | null;
68
- ORDER?: Partial<Record<keyof T, OrderDirection>>;
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<T>;
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: { ...pkFullValues },
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: { ...pkFullValues },
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: { ...pkFullValues },
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
- const allowListCache = new Map<string, AllowListCacheEntry>();
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 parseAllowList = (raw: string, sourcePath: string): string[] => {
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(normalizeSql)
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 (allowListPath: string): Promise<Set<string>> => {
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 cached = allowListCache.get(allowListPath);
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
- allowListCache.set(allowListPath, {
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(normalizeSql)
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(normalizeSql)
248
+ .map((entry) => normalizeSqlWith(entry, sqlQueryNormalizer))
216
249
  .filter((entry) => entry.length > 0)
217
250
  )).sort();
218
251