@carbonorm/carbonnode 6.0.12 → 6.0.13
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/executors/SqlExecutor.d.ts +1 -0
- package/dist/handlers/ExpressHandler.d.ts +6 -14
- package/dist/index.cjs.js +164 -40
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +164 -40
- package/dist/index.esm.js.map +1 -1
- package/dist/types/ormInterfaces.d.ts +13 -3
- package/dist/utils/cacheManager.d.ts +2 -3
- package/package.json +1 -1
- package/src/__tests__/convertForRequestBody.test.ts +58 -0
- package/src/__tests__/expressServer.e2e.test.ts +62 -38
- package/src/__tests__/fixtures/createTestServer.ts +7 -3
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +97 -60
- package/src/__tests__/logSql.test.ts +13 -0
- 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 +11 -11
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +4 -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 +6 -6
- 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 +3 -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 +3 -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 +3 -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 +6 -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 +3 -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 +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 +3 -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 +3 -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.post.json +4 -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.test.ts +46 -0
- package/src/api/convertForRequestBody.ts +9 -2
- package/src/api/restRequest.ts +1 -0
- package/src/executors/HttpExecutor.ts +1 -1
- package/src/executors/SqlExecutor.ts +64 -2
- package/src/handlers/ExpressHandler.ts +50 -39
- package/src/orm/builders/ConditionBuilder.ts +43 -1
- package/src/orm/queries/PostQueryBuilder.ts +24 -12
- package/src/types/ormInterfaces.ts +15 -3
- package/src/utils/cacheManager.ts +3 -4
- package/src/utils/colorSql.ts +18 -0
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"insertId": 16050,
|
|
4
4
|
"rest": [],
|
|
5
5
|
"sql": {
|
|
6
|
-
"sql": "INSERT INTO `rental` (\n `rental_date`, `inventory_id`, `customer_id`, `return_date`, `staff_id`, `last_update`\n ) VALUES
|
|
6
|
+
"sql": "INSERT INTO `rental` (\n `rental_date`, `inventory_id`, `customer_id`, `return_date`, `staff_id`, `last_update`\n ) VALUES\n (?, ?, ?, ?, ?, ?)",
|
|
7
7
|
"values": [
|
|
8
|
-
"2026-02-
|
|
8
|
+
"2026-02-08 19:33:00",
|
|
9
9
|
1,
|
|
10
10
|
1,
|
|
11
|
-
"2026-02-
|
|
11
|
+
"2026-02-08 19:33:00",
|
|
12
12
|
1,
|
|
13
|
-
"2026-02-
|
|
13
|
+
"2026-02-08 19:33:00"
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"rest": [
|
|
3
3
|
{
|
|
4
4
|
"rental_id": 16050,
|
|
5
|
-
"rental_date": "2026-02-
|
|
5
|
+
"rental_date": "2026-02-08T19:33:00.000Z",
|
|
6
6
|
"inventory_id": 1,
|
|
7
7
|
"customer_id": 1,
|
|
8
|
-
"return_date": "2026-02-
|
|
8
|
+
"return_date": "2026-02-08T19:33:00.000Z",
|
|
9
9
|
"staff_id": 1,
|
|
10
|
-
"last_update": "2026-02-
|
|
10
|
+
"last_update": "2026-02-08T19:33:00.000Z"
|
|
11
11
|
}
|
|
12
12
|
],
|
|
13
13
|
"sql": {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"rest": [
|
|
3
3
|
{
|
|
4
4
|
"rental_id": 16050,
|
|
5
|
-
"rental_date": "2026-02-
|
|
5
|
+
"rental_date": "2026-02-08T19:33:00.000Z",
|
|
6
6
|
"inventory_id": 1,
|
|
7
7
|
"customer_id": 1,
|
|
8
|
-
"return_date": "2026-02-
|
|
8
|
+
"return_date": "2026-02-08T19:33:00.000Z",
|
|
9
9
|
"staff_id": 1,
|
|
10
|
-
"last_update": "2026-02-
|
|
10
|
+
"last_update": "2026-02-08T19:33:00.000Z"
|
|
11
11
|
}
|
|
12
12
|
],
|
|
13
13
|
"sql": {
|
|
@@ -96,6 +96,52 @@ describe('SQL Builders', () => {
|
|
|
96
96
|
expect(params).toEqual([JSON.stringify(payload)]);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it('builds multi-row INSERT from dataInsertMultipleRows', () => {
|
|
100
|
+
const config = buildTestConfig();
|
|
101
|
+
const qb = new PostQueryBuilder(config as any, {
|
|
102
|
+
dataInsertMultipleRows: [
|
|
103
|
+
{
|
|
104
|
+
'actor.first_name': 'ALICE',
|
|
105
|
+
'actor.last_name': 'ONE',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
'actor.first_name': 'BOB',
|
|
109
|
+
'actor.last_name': 'TWO',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
} as any, false);
|
|
113
|
+
|
|
114
|
+
const { sql, params } = qb.build('actor');
|
|
115
|
+
|
|
116
|
+
expect(sql).toContain('INSERT INTO `actor`');
|
|
117
|
+
expect(sql).toContain('`first_name`, `last_name`');
|
|
118
|
+
expect(sql).toContain(') VALUES');
|
|
119
|
+
expect(sql).toContain('),');
|
|
120
|
+
expect(params).toEqual(['ALICE', 'ONE', 'BOB', 'TWO']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('builds multi-row INSERT from direct array request syntax', () => {
|
|
124
|
+
const config = buildTestConfig();
|
|
125
|
+
const qb = new PostQueryBuilder(config as any, [
|
|
126
|
+
{
|
|
127
|
+
'actor.first_name': 'ALICE',
|
|
128
|
+
'actor.last_name': 'ONE',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
'actor.first_name': 'BOB',
|
|
132
|
+
'actor.last_name': 'TWO',
|
|
133
|
+
},
|
|
134
|
+
] as any, false);
|
|
135
|
+
|
|
136
|
+
const { sql, params } = qb.build('actor');
|
|
137
|
+
|
|
138
|
+
expect(sql).toContain('INSERT INTO `actor`');
|
|
139
|
+
expect(sql).toContain('`first_name`, `last_name`');
|
|
140
|
+
expect(sql).toContain(') VALUES');
|
|
141
|
+
expect(sql).toContain('),');
|
|
142
|
+
expect(params).toEqual(['ALICE', 'ONE', 'BOB', 'TWO']);
|
|
143
|
+
});
|
|
144
|
+
|
|
99
145
|
it('stringifies dotted-key JSON payloads for JSON columns on UPDATE', () => {
|
|
100
146
|
const config = buildTestConfig();
|
|
101
147
|
const payload = { 'section1.preparedBy': 'Prepared by Assessorly, Co.' };
|
|
@@ -11,7 +11,11 @@ export default function <
|
|
|
11
11
|
restfulObject: RequestQueryBody<RequestMethod, RestTableInterface, CustomAndRequiredFields, RequestTableOverrides>,
|
|
12
12
|
tableName: string | string[],
|
|
13
13
|
C6: iC6Object,
|
|
14
|
-
regexErrorHandler: (message: string) => void =
|
|
14
|
+
regexErrorHandler: (message: string) => void = (message: string) => {
|
|
15
|
+
if (typeof globalThis !== "undefined" && typeof (globalThis as any).alert === "function") {
|
|
16
|
+
(globalThis as any).alert(message);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
15
19
|
) {
|
|
16
20
|
const payload: Record<string, any> = {};
|
|
17
21
|
const tableNames = Array.isArray(tableName) ? tableName : [tableName];
|
|
@@ -43,7 +47,8 @@ export default function <
|
|
|
43
47
|
C6Constants.DELETE,
|
|
44
48
|
C6Constants.WHERE,
|
|
45
49
|
C6Constants.JOIN,
|
|
46
|
-
C6Constants.PAGINATION
|
|
50
|
+
C6Constants.PAGINATION,
|
|
51
|
+
"cacheResults",
|
|
47
52
|
].includes(value)) {
|
|
48
53
|
const val = restfulObject[value];
|
|
49
54
|
if (Array.isArray(val)) {
|
|
@@ -52,6 +57,8 @@ export default function <
|
|
|
52
57
|
payload[value] = Object.keys(val)
|
|
53
58
|
.sort()
|
|
54
59
|
.reduce((acc, key) => ({ ...acc, [key]: val[key] }), {});
|
|
60
|
+
} else {
|
|
61
|
+
payload[value] = val;
|
|
55
62
|
}
|
|
56
63
|
continue;
|
|
57
64
|
}
|
package/src/api/restRequest.ts
CHANGED
|
@@ -257,7 +257,7 @@ export class HttpExecutor<
|
|
|
257
257
|
// literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
|
|
258
258
|
let querySerialized: string = sortAndSerializeQueryObject(tables, cacheRequestData ?? {});
|
|
259
259
|
|
|
260
|
-
let cachedRequest:
|
|
260
|
+
let cachedRequest: Promise<{ data: ResponseDataType }> | false = false;
|
|
261
261
|
|
|
262
262
|
if (cacheResults) {
|
|
263
263
|
cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData);
|
|
@@ -6,6 +6,7 @@ import { OrmGenerics } from "../types/ormGenerics";
|
|
|
6
6
|
import { C6Constants as C6C } from "../constants/C6Constants";
|
|
7
7
|
import {
|
|
8
8
|
DetermineResponseDataType,
|
|
9
|
+
iCacheResponse,
|
|
9
10
|
iRestLifecycleResponse,
|
|
10
11
|
iRestMethods,
|
|
11
12
|
iRestSqlExecutionContext,
|
|
@@ -15,7 +16,9 @@ import namedPlaceholders from 'named-placeholders';
|
|
|
15
16
|
import type { PoolConnection } from 'mysql2/promise';
|
|
16
17
|
import { Buffer } from 'buffer';
|
|
17
18
|
import { Executor } from "./Executor";
|
|
19
|
+
import {checkCache, setCache} from "../utils/cacheManager";
|
|
18
20
|
import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
|
|
21
|
+
import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
|
|
19
22
|
import { loadSqlAllowList, normalizeSql } from "../utils/sqlAllowList";
|
|
20
23
|
import { getLogContext, LogLevel, logWithLevel } from "../utils/logLevel";
|
|
21
24
|
|
|
@@ -483,11 +486,56 @@ export class SqlExecutor<
|
|
|
483
486
|
const method = this.config.requestMethod;
|
|
484
487
|
const tableName = this.config.restModel.TABLE_NAME;
|
|
485
488
|
const logContext = getLogContext(this.config, this.request);
|
|
486
|
-
const
|
|
489
|
+
const cacheResults = method === C6C.GET
|
|
490
|
+
&& !this.config.sqlAllowListPath
|
|
491
|
+
&& (this.request as { cacheResults?: boolean })?.cacheResults !== false;
|
|
492
|
+
|
|
493
|
+
const cacheRequestData = cacheResults
|
|
494
|
+
? JSON.parse(JSON.stringify(this.request ?? {}))
|
|
495
|
+
: undefined;
|
|
496
|
+
|
|
497
|
+
const requestArgumentsSerialized = cacheResults
|
|
498
|
+
? sortAndSerializeQueryObject(tableName, cacheRequestData ?? {})
|
|
499
|
+
: undefined;
|
|
500
|
+
|
|
501
|
+
if (cacheResults) {
|
|
502
|
+
const cachedRequest = checkCache<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>(
|
|
503
|
+
method,
|
|
504
|
+
tableName,
|
|
505
|
+
cacheRequestData,
|
|
506
|
+
);
|
|
507
|
+
if (cachedRequest) {
|
|
508
|
+
return (await cachedRequest).data;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
487
511
|
|
|
488
|
-
|
|
512
|
+
const sqlExecution = this.buildSqlExecutionContext(method, tableName, logContext);
|
|
513
|
+
const queryPromise = this.withConnection(async (conn) =>
|
|
489
514
|
this.executeQueryWithLifecycle(conn, method, sqlExecution, logContext),
|
|
490
515
|
);
|
|
516
|
+
|
|
517
|
+
if (!cacheResults || !cacheRequestData || !requestArgumentsSerialized) {
|
|
518
|
+
return await queryPromise;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const cacheRequest = queryPromise.then((data) =>
|
|
522
|
+
this.createCacheResponseEnvelope(method, tableName, data),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
setCache(method, tableName, cacheRequestData, {
|
|
526
|
+
requestArgumentsSerialized,
|
|
527
|
+
request: cacheRequest,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const cacheResponse = await cacheRequest;
|
|
531
|
+
setCache(method, tableName, cacheRequestData, {
|
|
532
|
+
requestArgumentsSerialized,
|
|
533
|
+
request: cacheRequest,
|
|
534
|
+
response: cacheResponse,
|
|
535
|
+
final: true,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return cacheResponse.data;
|
|
491
539
|
}
|
|
492
540
|
|
|
493
541
|
private getQueryBuilder(
|
|
@@ -573,6 +621,20 @@ export class SqlExecutor<
|
|
|
573
621
|
return { data };
|
|
574
622
|
}
|
|
575
623
|
|
|
624
|
+
private createCacheResponseEnvelope(
|
|
625
|
+
method: iRestMethods,
|
|
626
|
+
tableName: string,
|
|
627
|
+
data: DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>,
|
|
628
|
+
): iCacheResponse<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
629
|
+
return {
|
|
630
|
+
data,
|
|
631
|
+
config: {
|
|
632
|
+
method: method.toLowerCase(),
|
|
633
|
+
url: `/rest/${tableName}`,
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
576
638
|
private async executeQueryWithLifecycle(
|
|
577
639
|
conn: PoolConnection,
|
|
578
640
|
method: iRestMethods,
|
|
@@ -1,42 +1,45 @@
|
|
|
1
|
-
import type {Request, Response,
|
|
2
|
-
import type {Pool} from "mysql2/promise";
|
|
1
|
+
import type {Request, Response, Router} from "express";
|
|
3
2
|
import {C6C} from "../constants/C6Constants";
|
|
4
3
|
import restRequest from "../api/restRequest";
|
|
5
|
-
import
|
|
4
|
+
import {iRest, iRestMethods} from "../types/ormInterfaces";
|
|
6
5
|
import {LogLevel, logWithLevel} from "../utils/logLevel";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
routePath
|
|
23
|
-
...handlerConfig
|
|
24
|
-
}: iRestExpressRequestConfig) {
|
|
25
|
-
router.all(routePath, ExpressHandler(handlerConfig));
|
|
6
|
+
import {OrmGenerics} from "../types/ormGenerics";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export function restExpressRequest<G extends OrmGenerics>(
|
|
10
|
+
routerConfig: {
|
|
11
|
+
router: Pick<Router, "all">;
|
|
12
|
+
routePath?: string;
|
|
13
|
+
} &
|
|
14
|
+
Omit<
|
|
15
|
+
iRest<G['RestShortTableName'], G['RestTableInterface']>,
|
|
16
|
+
"requestMethod" | "restModel"
|
|
17
|
+
>
|
|
18
|
+
) {
|
|
19
|
+
const {router, routePath = "/rest/:table{/:primary}", ...handlerConfig} = routerConfig;
|
|
20
|
+
|
|
21
|
+
router.all(routePath, ExpressHandler<G>(handlerConfig));
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
|
|
29
24
|
// TODO - WE MUST make this a generic - optional, but helpful
|
|
30
25
|
// note sure how it would help anyone actually...
|
|
31
|
-
export function ExpressHandler
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
export function ExpressHandler<
|
|
27
|
+
G extends OrmGenerics
|
|
28
|
+
>(configX: (() => Omit<
|
|
29
|
+
iRest<G['RestShortTableName'], G['RestTableInterface']>,
|
|
30
|
+
"requestMethod" | "restModel"
|
|
31
|
+
>) | Omit<
|
|
32
|
+
iRest<G['RestShortTableName'], G['RestTableInterface']>,
|
|
33
|
+
"requestMethod" | "restModel"
|
|
34
|
+
>) {
|
|
35
|
+
|
|
36
|
+
return async (req: Request, res: Response) => {
|
|
39
37
|
try {
|
|
38
|
+
const config = typeof configX === "function" ? configX() : configX;
|
|
39
|
+
const {
|
|
40
|
+
C6
|
|
41
|
+
} = config;
|
|
42
|
+
|
|
40
43
|
const incomingMethod = req.method.toUpperCase() as iRestMethods;
|
|
41
44
|
const table = req.params.table;
|
|
42
45
|
let primary = req.params.primary;
|
|
@@ -47,11 +50,21 @@ export function ExpressHandler({
|
|
|
47
50
|
const treatAsGet = incomingMethod === 'POST' && methodOverride === 'GET';
|
|
48
51
|
|
|
49
52
|
const method: iRestMethods = treatAsGet ? 'GET' : incomingMethod;
|
|
50
|
-
const payload: any = treatAsGet ? {
|
|
53
|
+
const payload: any = treatAsGet ? {...(req.body as any)} : (method === 'GET' ? req.query : req.body);
|
|
54
|
+
|
|
55
|
+
// Query strings are text; coerce known boolean controls.
|
|
56
|
+
if (typeof payload?.cacheResults === "string") {
|
|
57
|
+
const normalized = payload.cacheResults.toLowerCase();
|
|
58
|
+
if (normalized === "false") payload.cacheResults = false;
|
|
59
|
+
if (normalized === "true") payload.cacheResults = true;
|
|
60
|
+
}
|
|
51
61
|
|
|
52
62
|
// Remove transport-only METHOD flag so it never leaks into ORM parsing
|
|
53
63
|
if (treatAsGet && 'METHOD' in payload) {
|
|
54
|
-
try {
|
|
64
|
+
try {
|
|
65
|
+
delete (payload as any).METHOD
|
|
66
|
+
} catch { /* noop */
|
|
67
|
+
}
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
// Warn for unsupported overrides but continue normally
|
|
@@ -121,10 +134,7 @@ export function ExpressHandler({
|
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
const response = await restRequest({
|
|
124
|
-
|
|
125
|
-
mysqlPool,
|
|
126
|
-
sqlAllowListPath,
|
|
127
|
-
websocketBroadcast,
|
|
137
|
+
...config,
|
|
128
138
|
requestMethod: method,
|
|
129
139
|
restModel: C6.TABLES[table]
|
|
130
140
|
})(payload);
|
|
@@ -132,8 +142,9 @@ export function ExpressHandler({
|
|
|
132
142
|
res.status(200).json({success: true, ...response});
|
|
133
143
|
|
|
134
144
|
} catch (err) {
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
+
logWithLevel(LogLevel.ERROR, undefined, console.error, message);
|
|
147
|
+
res.status(500).json({success: false, error: message});
|
|
137
148
|
}
|
|
138
149
|
};
|
|
139
150
|
}
|
|
@@ -444,9 +444,51 @@ export abstract class ConditionBuilder<
|
|
|
444
444
|
return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
|
|
447
|
+
let sql: string;
|
|
448
|
+
let isReference: boolean;
|
|
449
|
+
let isExpression: boolean;
|
|
450
|
+
let isSubSelect: boolean;
|
|
451
|
+
|
|
452
|
+
const shouldStringifyObjectFallback = (candidate: any): boolean => {
|
|
453
|
+
if (
|
|
454
|
+
typeof candidate !== 'object'
|
|
455
|
+
|| candidate === null
|
|
456
|
+
|| candidate instanceof Date
|
|
457
|
+
|| (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(candidate))
|
|
458
|
+
) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const normalizedCandidate = candidate instanceof Map
|
|
463
|
+
? Object.fromEntries(candidate)
|
|
464
|
+
: candidate;
|
|
465
|
+
const entries = Object.entries(normalizedCandidate as Record<string, any>);
|
|
466
|
+
|
|
467
|
+
if (entries.length !== 1) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const [key] = entries[0];
|
|
472
|
+
if (this.isOperator(key) || this.BOOLEAN_OPERATORS.has(key)) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return true;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
({ sql, isReference, isExpression, isSubSelect } = this.serializeOperand(normalized, params, contextColumn));
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (shouldStringifyObjectFallback(normalized)) {
|
|
483
|
+
return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
|
|
484
|
+
}
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
448
487
|
|
|
449
488
|
if (!isReference && !isExpression && !isSubSelect && typeof normalized === 'object' && normalized !== null) {
|
|
489
|
+
if (shouldStringifyObjectFallback(normalized)) {
|
|
490
|
+
return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
|
|
491
|
+
}
|
|
450
492
|
throw new Error('Unsupported operand type in SQL expression.');
|
|
451
493
|
}
|
|
452
494
|
|
|
@@ -18,25 +18,37 @@ export class PostQueryBuilder<G extends OrmGenerics> extends ConditionBuilder<G>
|
|
|
18
18
|
build(table: string) {
|
|
19
19
|
this.aliasMap = {};
|
|
20
20
|
const verb = C6C.REPLACE in this.request ? C6C.REPLACE : C6C.INSERT;
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const directRows = Array.isArray(this.request)
|
|
22
|
+
? this.request
|
|
23
|
+
: [];
|
|
24
|
+
const rows: Record<string, any>[] = directRows.length > 0
|
|
25
|
+
? directRows
|
|
26
|
+
: Array.isArray(this.request.dataInsertMultipleRows) &&
|
|
27
|
+
this.request.dataInsertMultipleRows.length > 0
|
|
28
|
+
? this.request.dataInsertMultipleRows
|
|
29
|
+
: [verb in this.request ? this.request[verb] : this.request];
|
|
30
|
+
const keys = Object.keys(rows[0] ?? {});
|
|
23
31
|
const params: any[] | Record<string, any> = this.useNamedParams ? {} : [];
|
|
24
|
-
const
|
|
32
|
+
const rowPlaceholders: string[] = [];
|
|
25
33
|
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const placeholders: string[] = [];
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = row[key] ?? null;
|
|
39
|
+
const trimmed = this.trimTablePrefix(table, key);
|
|
40
|
+
const qualified = `${table}.${trimmed}`;
|
|
41
|
+
const placeholder = this.serializeUpdateValue(value, params, qualified);
|
|
42
|
+
placeholders.push(placeholder);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
rowPlaceholders.push(`(${placeholders.join(', ')})`);
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
let sql = `${verb} INTO \`${table}\` (
|
|
36
49
|
${keys.map(k => `\`${this.trimTablePrefix(table, k)}\``).join(', ')}
|
|
37
|
-
) VALUES
|
|
38
|
-
${
|
|
39
|
-
)`;
|
|
50
|
+
) VALUES
|
|
51
|
+
${rowPlaceholders.join(',\n ')}`;
|
|
40
52
|
|
|
41
53
|
if (C6C.UPDATE in this.request) {
|
|
42
54
|
const updateData = this.request[C6C.UPDATE];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {AxiosInstance,
|
|
1
|
+
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";
|
|
@@ -101,10 +101,22 @@ export type RequestQueryBody<
|
|
|
101
101
|
? iAPI<RequestGetPutDeleteBody<Modify<T, Overrides> & Custom>>
|
|
102
102
|
: iAPI<RequestPostBody<Modify<T, Overrides> & Custom>>;
|
|
103
103
|
|
|
104
|
+
export interface iCacheRequestConfig {
|
|
105
|
+
method?: string;
|
|
106
|
+
url?: string;
|
|
107
|
+
headers?: any;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface iCacheResponse<ResponseDataType = any> {
|
|
111
|
+
data: ResponseDataType;
|
|
112
|
+
config?: iCacheRequestConfig;
|
|
113
|
+
[key: string]: any;
|
|
114
|
+
}
|
|
115
|
+
|
|
104
116
|
export interface iCacheAPI<ResponseDataType = any> {
|
|
105
117
|
requestArgumentsSerialized: string;
|
|
106
|
-
request:
|
|
107
|
-
response?:
|
|
118
|
+
request: Promise<iCacheResponse<ResponseDataType>>;
|
|
119
|
+
response?: iCacheResponse<ResponseDataType> & {
|
|
108
120
|
__carbonTiming?: {
|
|
109
121
|
start: number;
|
|
110
122
|
end: number;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {iCacheAPI} from "../types/ormInterfaces";
|
|
1
|
+
import type {iCacheAPI, iCacheResponse} from "../types/ormInterfaces";
|
|
3
2
|
import {LogLevel, logWithLevel, shouldLog} from "./logLevel";
|
|
4
3
|
|
|
5
4
|
// -----------------------------------------------------------------------------
|
|
@@ -61,13 +60,13 @@ export function checkCache<ResponseDataType = any>(
|
|
|
61
60
|
method: string,
|
|
62
61
|
tableName: string | string[],
|
|
63
62
|
requestData: any,
|
|
64
|
-
):
|
|
63
|
+
): Promise<iCacheResponse<ResponseDataType>> | false {
|
|
65
64
|
const key = makeCacheKey(method, tableName, requestData);
|
|
66
65
|
const cached = apiRequestCache.get(key);
|
|
67
66
|
|
|
68
67
|
if (!cached) return false;
|
|
69
68
|
|
|
70
|
-
if (shouldLog(LogLevel.
|
|
69
|
+
if (shouldLog(LogLevel.INFO, undefined)) {
|
|
71
70
|
console.groupCollapsed(
|
|
72
71
|
`%c API cache hit for ${method} ${tableName}`,
|
|
73
72
|
"color:#0c0",
|
package/src/utils/colorSql.ts
CHANGED
|
@@ -73,6 +73,23 @@ function collapseBinds(sql: string): string {
|
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* ( ? ×9 ), ( ? ×9 ), ( ? ×9 ) -> ( ? ×9 ) ×3
|
|
78
|
+
*/
|
|
79
|
+
function collapseRepeatedValueRows(sql: string): string {
|
|
80
|
+
const repeatedRowPattern =
|
|
81
|
+
/(\((?:\x1b\[[0-9;]*m)?\?\s*×\d+(?:\x1b\[[0-9;]*m)?\)|\(\s*(?:\?\s*,\s*)+\?\s*\))(?:\s*,\s*\1){2,}/g;
|
|
82
|
+
|
|
83
|
+
return sql.replace(repeatedRowPattern, (match, row: string) => {
|
|
84
|
+
const rowMatches = match.match(new RegExp(row.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"));
|
|
85
|
+
const count = rowMatches?.length ?? 1;
|
|
86
|
+
const normalizedRow = row.includes("×")
|
|
87
|
+
? row
|
|
88
|
+
: `(${C.DIM}? ×${(row.match(/\?/g) ?? []).length}${RESET})`;
|
|
89
|
+
return `${normalizedRow} ${C.DIM}×${count}${RESET}`;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
/* ---------- main formatter ---------- */
|
|
77
94
|
|
|
78
95
|
export default function colorSql(sql: string): string {
|
|
@@ -80,6 +97,7 @@ export default function colorSql(sql: string): string {
|
|
|
80
97
|
|
|
81
98
|
/* 1️⃣ collapse bind noise */
|
|
82
99
|
s = collapseBinds(s);
|
|
100
|
+
s = collapseRepeatedValueRows(s);
|
|
83
101
|
|
|
84
102
|
/* 2️⃣ table.column coloring (core visual grouping) */
|
|
85
103
|
s = s.replace(
|