@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
|
@@ -27,6 +27,43 @@ import { getLogContext, LogLevel, logWithLevel } from "../utils/logLevel";
|
|
|
27
27
|
|
|
28
28
|
const SQL_ALLOWLIST_BLOCKED_CODE = "SQL_ALLOWLIST_BLOCKED";
|
|
29
29
|
|
|
30
|
+
const fillRandomBytes = (bytes: Uint8Array): void => {
|
|
31
|
+
const cryptoRef = (globalThis as { crypto?: Crypto }).crypto;
|
|
32
|
+
if (!cryptoRef || typeof cryptoRef.getRandomValues !== "function") {
|
|
33
|
+
throw new Error("Secure random source unavailable: crypto.getRandomValues is required for UUID generation.");
|
|
34
|
+
}
|
|
35
|
+
cryptoRef.getRandomValues(bytes);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const generateUuidV7 = (): string => {
|
|
39
|
+
const bytes = new Uint8Array(16);
|
|
40
|
+
const random = new Uint8Array(10);
|
|
41
|
+
fillRandomBytes(random);
|
|
42
|
+
|
|
43
|
+
const timestampMs = Date.now();
|
|
44
|
+
bytes[0] = Math.floor(timestampMs / 1099511627776) & 0xff; // 2^40
|
|
45
|
+
bytes[1] = Math.floor(timestampMs / 4294967296) & 0xff; // 2^32
|
|
46
|
+
bytes[2] = Math.floor(timestampMs / 16777216) & 0xff; // 2^24
|
|
47
|
+
bytes[3] = Math.floor(timestampMs / 65536) & 0xff; // 2^16
|
|
48
|
+
bytes[4] = Math.floor(timestampMs / 256) & 0xff; // 2^8
|
|
49
|
+
bytes[5] = timestampMs & 0xff;
|
|
50
|
+
|
|
51
|
+
// RFC 9562 UUIDv7 layout
|
|
52
|
+
bytes[6] = 0x70 | (random[0] & 0x0f); // version 7 + rand_a high bits
|
|
53
|
+
bytes[7] = random[1]; // rand_a low bits
|
|
54
|
+
bytes[8] = 0x80 | (random[2] & 0x3f); // variant + rand_b high bits
|
|
55
|
+
bytes[9] = random[3];
|
|
56
|
+
bytes[10] = random[4];
|
|
57
|
+
bytes[11] = random[5];
|
|
58
|
+
bytes[12] = random[6];
|
|
59
|
+
bytes[13] = random[7];
|
|
60
|
+
bytes[14] = random[8];
|
|
61
|
+
bytes[15] = random[9];
|
|
62
|
+
|
|
63
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
64
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
30
67
|
export type SqlAllowListBlockedError = Error & {
|
|
31
68
|
code: typeof SQL_ALLOWLIST_BLOCKED_CODE;
|
|
32
69
|
tableName?: string;
|
|
@@ -72,6 +109,157 @@ const createSqlAllowListBlockedError = (args: {
|
|
|
72
109
|
export class SqlExecutor<
|
|
73
110
|
G extends OrmGenerics
|
|
74
111
|
> extends Executor<G> {
|
|
112
|
+
private getPostRequestRows(): Record<string, any>[] {
|
|
113
|
+
const request = this.request as any;
|
|
114
|
+
if (!request) return [];
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(request)) {
|
|
117
|
+
return request as Record<string, any>[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
Array.isArray(request.dataInsertMultipleRows)
|
|
122
|
+
&& request.dataInsertMultipleRows.length > 0
|
|
123
|
+
) {
|
|
124
|
+
return request.dataInsertMultipleRows as Record<string, any>[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const verb = C6C.REPLACE in request ? C6C.REPLACE : C6C.INSERT;
|
|
128
|
+
if (verb in request && request[verb] && typeof request[verb] === "object") {
|
|
129
|
+
return [request[verb] as Record<string, any>];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof request === "object") {
|
|
133
|
+
return [request as Record<string, any>];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getTypeValidationForColumn(shortKey: string, fullKey: string): Record<string, any> | undefined {
|
|
140
|
+
const validation = (this.config.restModel as any)?.TYPE_VALIDATION;
|
|
141
|
+
if (!validation || typeof validation !== "object") return undefined;
|
|
142
|
+
return validation[shortKey] ?? validation[fullKey];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private isUuidLikePrimaryColumn(columnDef: Record<string, any> | undefined): boolean {
|
|
146
|
+
if (!columnDef || typeof columnDef !== "object") return false;
|
|
147
|
+
if (columnDef.AUTO_INCREMENT === true) return false;
|
|
148
|
+
|
|
149
|
+
const mysqlType = String(columnDef.MYSQL_TYPE ?? "").toLowerCase();
|
|
150
|
+
const maxLength = String(columnDef.MAX_LENGTH ?? "").trim();
|
|
151
|
+
|
|
152
|
+
if (mysqlType.includes("uuid")) return true;
|
|
153
|
+
|
|
154
|
+
const isBinary16 = mysqlType.includes("binary")
|
|
155
|
+
&& (maxLength === "16" || /\b16\b/.test(mysqlType) || mysqlType === "binary");
|
|
156
|
+
const isUuidString = (mysqlType.includes("char") || mysqlType.includes("varchar"))
|
|
157
|
+
&& (maxLength === "32" || maxLength === "36");
|
|
158
|
+
|
|
159
|
+
return isBinary16 || isUuidString;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private hasDefinedValue(value: unknown): boolean {
|
|
163
|
+
if (value === undefined || value === null) return false;
|
|
164
|
+
if (typeof value === "string" && value.trim() === "") return false;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private generatePrimaryUuidValue(columnDef: Record<string, any>): string {
|
|
169
|
+
const mysqlType = String(columnDef.MYSQL_TYPE ?? "").toLowerCase();
|
|
170
|
+
const maxLength = String(columnDef.MAX_LENGTH ?? "").trim();
|
|
171
|
+
const uuid = generateUuidV7();
|
|
172
|
+
|
|
173
|
+
// BINARY(16) and CHAR/VARCHAR(32) commonly persist UUIDs as 32-hex.
|
|
174
|
+
if (mysqlType.includes("binary") || maxLength === "32") {
|
|
175
|
+
return uuid.replace(/-/g, "").toUpperCase();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return uuid;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private assignMissingPostPrimaryUuids(): void {
|
|
182
|
+
if (this.config.requestMethod !== C6C.POST) return;
|
|
183
|
+
|
|
184
|
+
const rows = this.getPostRequestRows();
|
|
185
|
+
if (rows.length === 0) return;
|
|
186
|
+
|
|
187
|
+
const columns = this.config.restModel.COLUMNS as Record<string, string>;
|
|
188
|
+
const tableName = this.config.restModel.TABLE_NAME as string;
|
|
189
|
+
const primaryShorts = this.config.restModel.PRIMARY_SHORT ?? [];
|
|
190
|
+
|
|
191
|
+
const primaryColumns = primaryShorts
|
|
192
|
+
.map((shortKey) => {
|
|
193
|
+
const fullKey = Object.keys(columns).find((key) => columns[key] === shortKey)
|
|
194
|
+
?? `${tableName}.${shortKey}`;
|
|
195
|
+
const columnDef = this.getTypeValidationForColumn(shortKey, fullKey);
|
|
196
|
+
return { shortKey, fullKey, columnDef };
|
|
197
|
+
})
|
|
198
|
+
.filter(({ columnDef }) => this.isUuidLikePrimaryColumn(columnDef));
|
|
199
|
+
|
|
200
|
+
if (primaryColumns.length === 0) return;
|
|
201
|
+
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
if (!row || typeof row !== "object") continue;
|
|
204
|
+
|
|
205
|
+
const useQualifiedKeyByDefault = Object.keys(row).some((key) => key.includes("."));
|
|
206
|
+
|
|
207
|
+
for (const primaryColumn of primaryColumns) {
|
|
208
|
+
const existing = row[primaryColumn.shortKey] ?? row[primaryColumn.fullKey];
|
|
209
|
+
if (this.hasDefinedValue(existing)) continue;
|
|
210
|
+
|
|
211
|
+
const generated = this.generatePrimaryUuidValue(primaryColumn.columnDef!);
|
|
212
|
+
if (Object.prototype.hasOwnProperty.call(row, primaryColumn.shortKey)) {
|
|
213
|
+
row[primaryColumn.shortKey] = generated;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (Object.prototype.hasOwnProperty.call(row, primaryColumn.fullKey)) {
|
|
217
|
+
row[primaryColumn.fullKey] = generated;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
row[useQualifiedKeyByDefault ? primaryColumn.fullKey : primaryColumn.shortKey] = generated;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private buildPostResponseRows(insertId?: number | string): Record<string, unknown>[] {
|
|
227
|
+
const rows = this.getPostRequestRows();
|
|
228
|
+
if (rows.length === 0) return [];
|
|
229
|
+
|
|
230
|
+
const columns = this.config.restModel.COLUMNS as Record<string, string>;
|
|
231
|
+
const validColumns = new Set(Object.values(columns));
|
|
232
|
+
const pkShorts = this.config.restModel.PRIMARY_SHORT ?? [];
|
|
233
|
+
const now = new Date().toISOString();
|
|
234
|
+
|
|
235
|
+
return rows.map((row, index) => {
|
|
236
|
+
const normalized = this.normalizeRequestPayload(row ?? {});
|
|
237
|
+
|
|
238
|
+
if (validColumns.has("changed_at") && normalized.changed_at === undefined) {
|
|
239
|
+
normalized.changed_at = now;
|
|
240
|
+
}
|
|
241
|
+
if (validColumns.has("created_at") && normalized.created_at === undefined) {
|
|
242
|
+
normalized.created_at = now;
|
|
243
|
+
}
|
|
244
|
+
if (validColumns.has("updated_at") && normalized.updated_at === undefined) {
|
|
245
|
+
normalized.updated_at = now;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// When DB generated PK is numeric/autoincrement, expose it for the single-row insert.
|
|
249
|
+
if (
|
|
250
|
+
index === 0
|
|
251
|
+
&& insertId !== undefined
|
|
252
|
+
&& insertId !== null
|
|
253
|
+
&& pkShorts.length === 1
|
|
254
|
+
&& !this.hasDefinedValue(normalized[pkShorts[0]])
|
|
255
|
+
) {
|
|
256
|
+
normalized[pkShorts[0]] = insertId;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return normalized;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
75
263
|
private resolveSqlLogMethod(method: iRestMethods, sql: string): string {
|
|
76
264
|
const token = sql.trim().split(/\s+/, 1)[0]?.toUpperCase();
|
|
77
265
|
if (token) return token;
|
|
@@ -112,6 +300,8 @@ export class SqlExecutor<
|
|
|
112
300
|
throw e;
|
|
113
301
|
}
|
|
114
302
|
|
|
303
|
+
this.assignMissingPostPrimaryUuids();
|
|
304
|
+
|
|
115
305
|
const logContext = getLogContext(this.config, this.request);
|
|
116
306
|
logWithLevel(
|
|
117
307
|
LogLevel.DEBUG,
|
|
@@ -248,6 +438,7 @@ export class SqlExecutor<
|
|
|
248
438
|
C6C.REPLACE,
|
|
249
439
|
"dataInsertMultipleRows",
|
|
250
440
|
"cacheResults",
|
|
441
|
+
"skipReactBootstrap",
|
|
251
442
|
"fetchDependencies",
|
|
252
443
|
"debug",
|
|
253
444
|
"success",
|
|
@@ -340,7 +531,7 @@ export class SqlExecutor<
|
|
|
340
531
|
}
|
|
341
532
|
|
|
342
533
|
if (value !== undefined) {
|
|
343
|
-
pkValues[pkShort] = value;
|
|
534
|
+
pkValues[pkShort] = this.unwrapPrimaryKeyValue(value);
|
|
344
535
|
}
|
|
345
536
|
}
|
|
346
537
|
|
|
@@ -351,6 +542,29 @@ export class SqlExecutor<
|
|
|
351
542
|
return Object.keys(pkValues).length > 0 ? pkValues : null;
|
|
352
543
|
}
|
|
353
544
|
|
|
545
|
+
private unwrapPrimaryKeyValue(value: any): any {
|
|
546
|
+
if (!Array.isArray(value)) return value;
|
|
547
|
+
|
|
548
|
+
if (value.length === 2) {
|
|
549
|
+
const [head, tail] = value;
|
|
550
|
+
if (head === C6C.EQUAL) {
|
|
551
|
+
return this.unwrapPrimaryKeyValue(tail);
|
|
552
|
+
}
|
|
553
|
+
if (head === C6C.LIT || head === C6C.PARAM) {
|
|
554
|
+
return tail;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (value.length === 3) {
|
|
559
|
+
const [, operator, right] = value;
|
|
560
|
+
if (operator === C6C.EQUAL) {
|
|
561
|
+
return this.unwrapPrimaryKeyValue(right);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
|
|
354
568
|
private extractPrimaryKeyValuesFromData(data: any): Record<string, any> | null {
|
|
355
569
|
if (!data) return null;
|
|
356
570
|
const row = Array.isArray(data) ? data[0] : data;
|
|
@@ -549,6 +763,9 @@ export class SqlExecutor<
|
|
|
549
763
|
const logContext = getLogContext(this.config, this.request);
|
|
550
764
|
const cacheResults = method === C6C.GET
|
|
551
765
|
&& (this.request.cacheResults ?? true);
|
|
766
|
+
const cacheAllowListStatus: SqlAllowListStatus = this.config.sqlAllowListPath
|
|
767
|
+
? "allowed"
|
|
768
|
+
: "not verified";
|
|
552
769
|
|
|
553
770
|
const cacheRequestData = cacheResults
|
|
554
771
|
? JSON.parse(JSON.stringify(this.request ?? {}))
|
|
@@ -560,7 +777,13 @@ export class SqlExecutor<
|
|
|
560
777
|
|
|
561
778
|
const evictFromCache =
|
|
562
779
|
method === C6C.GET && cacheResults && cacheRequestData
|
|
563
|
-
? () => evictCacheEntry(
|
|
780
|
+
? () => evictCacheEntry(
|
|
781
|
+
method,
|
|
782
|
+
tableName,
|
|
783
|
+
cacheRequestData,
|
|
784
|
+
logContext,
|
|
785
|
+
cacheAllowListStatus,
|
|
786
|
+
)
|
|
564
787
|
: undefined;
|
|
565
788
|
|
|
566
789
|
if (cacheResults) {
|
|
@@ -568,7 +791,8 @@ export class SqlExecutor<
|
|
|
568
791
|
method,
|
|
569
792
|
tableName,
|
|
570
793
|
cacheRequestData,
|
|
571
|
-
logContext
|
|
794
|
+
logContext,
|
|
795
|
+
cacheAllowListStatus,
|
|
572
796
|
);
|
|
573
797
|
if (cachedRequest) {
|
|
574
798
|
const cachedData = (await cachedRequest).data;
|
|
@@ -611,12 +835,14 @@ export class SqlExecutor<
|
|
|
611
835
|
setCache(method, tableName, cacheRequestData, {
|
|
612
836
|
requestArgumentsSerialized,
|
|
613
837
|
request: cacheRequest,
|
|
838
|
+
allowListStatus: cacheAllowListStatus,
|
|
614
839
|
});
|
|
615
840
|
|
|
616
841
|
const cacheResponse = await cacheRequest;
|
|
617
842
|
setCache(method, tableName, cacheRequestData, {
|
|
618
843
|
requestArgumentsSerialized,
|
|
619
844
|
request: cacheRequest,
|
|
845
|
+
allowListStatus: cacheAllowListStatus,
|
|
620
846
|
response: cacheResponse,
|
|
621
847
|
final: true,
|
|
622
848
|
});
|
|
@@ -695,7 +921,9 @@ export class SqlExecutor<
|
|
|
695
921
|
return {
|
|
696
922
|
affected: result.affectedRows as number,
|
|
697
923
|
insertId: result.insertId as number,
|
|
698
|
-
rest:
|
|
924
|
+
rest: method === C6C.POST
|
|
925
|
+
? this.buildPostResponseRows(result.insertId as number | string | undefined)
|
|
926
|
+
: [],
|
|
699
927
|
sql: { sql: sqlExecution.sql, values: sqlExecution.values },
|
|
700
928
|
} as DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>;
|
|
701
929
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./handlers/ExpressHandler";
|
|
|
19
19
|
export * from "./orm/queryHelpers";
|
|
20
20
|
export * from "./orm/builders/AggregateBuilder";
|
|
21
21
|
export * from "./orm/builders/ConditionBuilder";
|
|
22
|
+
export * from "./orm/builders/ExpressionSerializer";
|
|
22
23
|
export * from "./orm/builders/JoinBuilder";
|
|
23
24
|
export * from "./orm/builders/PaginationBuilder";
|
|
24
25
|
export * from "./orm/queries/DeleteQueryBuilder";
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import {Executor} from "../../executors/Executor";
|
|
2
2
|
import {OrmGenerics} from "../../types/ormGenerics";
|
|
3
|
-
import {
|
|
3
|
+
import {SQL_KNOWN_FUNCTIONS} from "../../types/mysqlTypes";
|
|
4
4
|
import {getLogContext, LogLevel, logWithLevel} from "../../utils/logLevel";
|
|
5
|
+
import {
|
|
6
|
+
iSerializedExpression,
|
|
7
|
+
serializeSqlExpression,
|
|
8
|
+
tSqlParams,
|
|
9
|
+
} from "./ExpressionSerializer";
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
const KNOWN_FUNCTION_LOOKUP = new Set(
|
|
12
|
+
SQL_KNOWN_FUNCTIONS.map((name) => String(name).toUpperCase()),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G> {
|
|
7
16
|
protected selectAliases: Set<string> = new Set<string>();
|
|
8
17
|
|
|
9
18
|
// Overridden in ConditionBuilder where alias tracking is available.
|
|
@@ -12,130 +21,82 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
12
21
|
// no-op placeholder for subclasses that do not implement alias validation
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
if (typeof
|
|
17
|
-
this.assertValidIdentifier(field, 'SELECT field');
|
|
18
|
-
return field;
|
|
19
|
-
}
|
|
24
|
+
protected isReferenceExpression(value: string): boolean {
|
|
25
|
+
if (typeof value !== 'string') return false;
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// If the array represents a tuple/literal list (e.g., [lng, lat]) rather than a
|
|
26
|
-
// function call like [FN, ...args], serialize the list as a comma-separated
|
|
27
|
-
// literal sequence so parent calls (like ORDER BY FN(<here>)) can embed it.
|
|
28
|
-
const isNumericString = (s: string) => /^-?\d+(?:\.\d+)?$/.test(String(s).trim());
|
|
29
|
-
if (typeof field[0] !== 'string' || isNumericString(field[0])) {
|
|
30
|
-
return field
|
|
31
|
-
.map((arg) => {
|
|
32
|
-
if (Array.isArray(arg)) return this.buildAggregateField(arg, params);
|
|
33
|
-
return String(arg);
|
|
34
|
-
})
|
|
35
|
-
.join(', ');
|
|
36
|
-
}
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (trimmed.length === 0) return false;
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
let alias: string | undefined;
|
|
30
|
+
if (trimmed === '*') return true;
|
|
40
31
|
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*\.\*$/.test(trimmed)) {
|
|
33
|
+
this.assertValidIdentifier(trimmed, 'SQL reference');
|
|
34
|
+
return true;
|
|
44
35
|
}
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (args.length === 1 && Array.isArray(args[0])) {
|
|
50
|
-
args = args[0];
|
|
37
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
|
|
38
|
+
this.assertValidIdentifier(trimmed, 'SQL reference');
|
|
39
|
+
return true;
|
|
51
40
|
}
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!params) {
|
|
56
|
-
throw new Error('PARAM requires parameter tracking.');
|
|
57
|
-
}
|
|
58
|
-
const value = args[0];
|
|
59
|
-
// Use empty column context; ORDER/SELECT literals have no column typing.
|
|
60
|
-
// @ts-ignore addParam is provided by ConditionBuilder in our hierarchy.
|
|
61
|
-
return this.addParam(params, '', value);
|
|
42
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed) && this.selectAliases.has(trimmed)) {
|
|
43
|
+
return true;
|
|
62
44
|
}
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
throw new Error('Scalar subselects in SELECT require parameter tracking.');
|
|
67
|
-
}
|
|
68
|
-
const subRequest = args[0];
|
|
69
|
-
const subSql = (this as any).buildScalarSubSelect?.(subRequest, params);
|
|
70
|
-
if (!subSql) {
|
|
71
|
-
throw new Error('Failed to build scalar subselect.');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
let expr = subSql;
|
|
75
|
-
if (alias) {
|
|
76
|
-
this.selectAliases.add(alias);
|
|
77
|
-
expr += ` AS ${alias}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
logWithLevel(
|
|
81
|
-
LogLevel.DEBUG,
|
|
82
|
-
getLogContext(this.config, this.request),
|
|
83
|
-
console.log,
|
|
84
|
-
`[SELECT] ${expr}`,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
return expr;
|
|
88
|
-
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
89
48
|
|
|
90
|
-
|
|
49
|
+
protected isKnownFunction(functionName: string): boolean {
|
|
50
|
+
return KNOWN_FUNCTION_LOOKUP.has(functionName.trim().toUpperCase());
|
|
51
|
+
}
|
|
91
52
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
53
|
+
protected serializeExpression(
|
|
54
|
+
expression: any,
|
|
55
|
+
params?: tSqlParams,
|
|
56
|
+
context: string = 'SQL expression',
|
|
57
|
+
contextColumn?: string,
|
|
58
|
+
): iSerializedExpression {
|
|
59
|
+
return serializeSqlExpression(expression, {
|
|
60
|
+
params,
|
|
61
|
+
context,
|
|
62
|
+
contextColumn,
|
|
63
|
+
hooks: {
|
|
64
|
+
assertValidIdentifier: (identifier: string, hookContext: string) => {
|
|
65
|
+
this.assertValidIdentifier(identifier, hookContext);
|
|
66
|
+
},
|
|
67
|
+
isReference: (value: string) => this.isReferenceExpression(value),
|
|
68
|
+
addParam: (target: tSqlParams, column: string, value: any) => {
|
|
69
|
+
const addParam = (this as any).addParam;
|
|
70
|
+
if (typeof addParam !== 'function') {
|
|
71
|
+
throw new Error('Expression literal binding requires addParam support.');
|
|
99
72
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (alreadyQuoted) {
|
|
107
|
-
return trimmed;
|
|
108
|
-
}
|
|
109
|
-
const escaped = arg.replace(/'/g, "''");
|
|
110
|
-
return `'${escaped}'`;
|
|
73
|
+
return addParam.call(this, target, column, value);
|
|
74
|
+
},
|
|
75
|
+
buildScalarSubSelect: (subRequest: any, target: tSqlParams) => {
|
|
76
|
+
const builder = (this as any).buildScalarSubSelect;
|
|
77
|
+
if (typeof builder !== 'function') {
|
|
78
|
+
throw new Error('Scalar subselects require SelectQueryBuilder context.');
|
|
111
79
|
}
|
|
80
|
+
return builder.call(this, subRequest, target);
|
|
81
|
+
},
|
|
82
|
+
onAlias: (alias: string) => {
|
|
83
|
+
this.selectAliases.add(alias);
|
|
84
|
+
},
|
|
85
|
+
isKnownFunction: (functionName: string) => this.isKnownFunction(functionName),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return String(arg);
|
|
116
|
-
})
|
|
117
|
-
.join(', ');
|
|
118
|
-
|
|
119
|
-
let expr: string;
|
|
120
|
-
|
|
121
|
-
if (F === 'DISTINCT') {
|
|
122
|
-
expr = `DISTINCT ${argList}`;
|
|
123
|
-
} else {
|
|
124
|
-
expr = `${F}(${argList})`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (alias) {
|
|
128
|
-
this.selectAliases.add(alias);
|
|
129
|
-
expr += ` AS ${alias}`;
|
|
130
|
-
}
|
|
90
|
+
buildAggregateField(field: string | any[], params?: tSqlParams): string {
|
|
91
|
+
const serialized = this.serializeExpression(field, params, 'SELECT expression');
|
|
131
92
|
|
|
132
93
|
logWithLevel(
|
|
133
94
|
LogLevel.DEBUG,
|
|
134
95
|
getLogContext(this.config, this.request),
|
|
135
96
|
console.log,
|
|
136
|
-
`[SELECT] ${
|
|
97
|
+
`[SELECT] ${serialized.sql}`,
|
|
137
98
|
);
|
|
138
99
|
|
|
139
|
-
return
|
|
100
|
+
return serialized.sql;
|
|
140
101
|
}
|
|
141
102
|
}
|