@carbonorm/carbonnode 6.0.14 → 6.0.18
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 +17 -0
- package/dist/index.cjs.js +450 -248
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +450 -249
- package/dist/index.esm.js.map +1 -1
- package/dist/types/ormInterfaces.d.ts +1 -0
- package/dist/utils/cacheManager.d.ts +3 -1
- package/dist/utils/logLevel.d.ts +3 -3
- package/dist/utils/logSql.d.ts +10 -1
- package/package.json +2 -2
- package/scripts/assets/handlebars/C6.ts.handlebars +1 -1
- package/src/__tests__/fixtures/sqlResponses/sqlAllowList.json +1 -1
- package/src/__tests__/httpExecutor.cacheEviction.test.ts +70 -0
- package/src/__tests__/logSql.test.ts +54 -2
- 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 +59 -70
- package/src/__tests__/sakila-db/C6.ts +2 -2
- package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +3 -3
- 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 +5 -5
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- 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 +5 -5
- 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 +2 -2
- 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 +1 -1
- 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 +2 -2
- 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 +2 -2
- 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 +3 -3
- 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 +100 -0
- package/src/__tests__/sqlBuilders.test.ts +3 -4
- package/src/__tests__/sqlExecutor.cacheEviction.test.ts +79 -0
- package/src/executors/HttpExecutor.ts +20 -4
- package/src/executors/SqlExecutor.ts +131 -12
- package/src/orm/queries/DeleteQueryBuilder.ts +0 -4
- package/src/orm/queries/PostQueryBuilder.ts +0 -4
- package/src/orm/queries/SelectQueryBuilder.ts +0 -4
- package/src/orm/queries/UpdateQueryBuilder.ts +0 -4
- package/src/types/ormInterfaces.ts +4 -1
- package/src/utils/cacheManager.ts +26 -9
- package/src/utils/logLevel.ts +3 -4
- package/src/utils/logSql.ts +51 -6
- package/src/utils/sqlAllowList.ts +111 -9
|
@@ -16,15 +16,76 @@ import namedPlaceholders from 'named-placeholders';
|
|
|
16
16
|
import type { PoolConnection } from 'mysql2/promise';
|
|
17
17
|
import { Buffer } from 'buffer';
|
|
18
18
|
import { Executor } from "./Executor";
|
|
19
|
-
import {checkCache, setCache} from "../utils/cacheManager";
|
|
19
|
+
import {checkCache, evictCacheEntry, setCache} from "../utils/cacheManager";
|
|
20
|
+
import logSql, {
|
|
21
|
+
SqlAllowListStatus,
|
|
22
|
+
} from "../utils/logSql";
|
|
20
23
|
import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
|
|
21
24
|
import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
|
|
22
25
|
import { loadSqlAllowList, normalizeSql } from "../utils/sqlAllowList";
|
|
23
26
|
import { getLogContext, LogLevel, logWithLevel } from "../utils/logLevel";
|
|
24
27
|
|
|
28
|
+
const SQL_ALLOWLIST_BLOCKED_CODE = "SQL_ALLOWLIST_BLOCKED";
|
|
29
|
+
|
|
30
|
+
export type SqlAllowListBlockedError = Error & {
|
|
31
|
+
code: typeof SQL_ALLOWLIST_BLOCKED_CODE;
|
|
32
|
+
tableName?: string;
|
|
33
|
+
method?: string;
|
|
34
|
+
normalizedSql: string;
|
|
35
|
+
allowListPath: string;
|
|
36
|
+
sqlAllowList: {
|
|
37
|
+
sql: string;
|
|
38
|
+
table: string | null;
|
|
39
|
+
method: string | null;
|
|
40
|
+
allowListPath: string;
|
|
41
|
+
canAdd: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createSqlAllowListBlockedError = (args: {
|
|
46
|
+
tableName?: string;
|
|
47
|
+
method?: string;
|
|
48
|
+
normalizedSql: string;
|
|
49
|
+
allowListPath: string;
|
|
50
|
+
}): SqlAllowListBlockedError => {
|
|
51
|
+
const error = new Error(
|
|
52
|
+
`SQL statement is not permitted by allowlist (${args.allowListPath}).`,
|
|
53
|
+
) as SqlAllowListBlockedError;
|
|
54
|
+
|
|
55
|
+
error.name = "SqlAllowListBlockedError";
|
|
56
|
+
error.code = SQL_ALLOWLIST_BLOCKED_CODE;
|
|
57
|
+
error.tableName = args.tableName;
|
|
58
|
+
error.method = args.method;
|
|
59
|
+
error.normalizedSql = args.normalizedSql;
|
|
60
|
+
error.allowListPath = args.allowListPath;
|
|
61
|
+
error.sqlAllowList = {
|
|
62
|
+
sql: args.normalizedSql,
|
|
63
|
+
table: args.tableName ?? null,
|
|
64
|
+
method: args.method ?? null,
|
|
65
|
+
allowListPath: args.allowListPath,
|
|
66
|
+
canAdd: true,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return error;
|
|
70
|
+
};
|
|
71
|
+
|
|
25
72
|
export class SqlExecutor<
|
|
26
73
|
G extends OrmGenerics
|
|
27
74
|
> extends Executor<G> {
|
|
75
|
+
private resolveSqlLogMethod(method: iRestMethods, sql: string): string {
|
|
76
|
+
const token = sql.trim().split(/\s+/, 1)[0]?.toUpperCase();
|
|
77
|
+
if (token) return token;
|
|
78
|
+
switch (method) {
|
|
79
|
+
case C6C.GET:
|
|
80
|
+
return "SELECT";
|
|
81
|
+
case C6C.POST:
|
|
82
|
+
return "INSERT";
|
|
83
|
+
case C6C.PUT:
|
|
84
|
+
return "UPDATE";
|
|
85
|
+
default:
|
|
86
|
+
return "DELETE";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
28
89
|
|
|
29
90
|
async execute(): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
30
91
|
const { TABLE_NAME } = this.config.restModel;
|
|
@@ -487,8 +548,7 @@ export class SqlExecutor<
|
|
|
487
548
|
const tableName = this.config.restModel.TABLE_NAME;
|
|
488
549
|
const logContext = getLogContext(this.config, this.request);
|
|
489
550
|
const cacheResults = method === C6C.GET
|
|
490
|
-
&&
|
|
491
|
-
&& (this.request as { cacheResults?: boolean })?.cacheResults !== false;
|
|
551
|
+
&& (this.request.cacheResults ?? true);
|
|
492
552
|
|
|
493
553
|
const cacheRequestData = cacheResults
|
|
494
554
|
? JSON.parse(JSON.stringify(this.request ?? {}))
|
|
@@ -498,29 +558,55 @@ export class SqlExecutor<
|
|
|
498
558
|
? sortAndSerializeQueryObject(tableName, cacheRequestData ?? {})
|
|
499
559
|
: undefined;
|
|
500
560
|
|
|
561
|
+
const evictFromCache =
|
|
562
|
+
method === C6C.GET && cacheResults && cacheRequestData
|
|
563
|
+
? () => evictCacheEntry(method, tableName, cacheRequestData)
|
|
564
|
+
: undefined;
|
|
565
|
+
|
|
501
566
|
if (cacheResults) {
|
|
502
567
|
const cachedRequest = checkCache<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>>(
|
|
503
568
|
method,
|
|
504
569
|
tableName,
|
|
505
570
|
cacheRequestData,
|
|
571
|
+
logContext
|
|
506
572
|
);
|
|
507
573
|
if (cachedRequest) {
|
|
508
|
-
|
|
574
|
+
const cachedData = (await cachedRequest).data;
|
|
575
|
+
if (evictFromCache
|
|
576
|
+
&& cachedData
|
|
577
|
+
&& typeof cachedData === "object"
|
|
578
|
+
&& Array.isArray((cachedData as DetermineResponseDataType<'GET', G['RestTableInterface']>).rest)) {
|
|
579
|
+
(cachedData as DetermineResponseDataType<'GET', G['RestTableInterface']>).evictFromCache = evictFromCache;
|
|
580
|
+
}
|
|
581
|
+
return cachedData;
|
|
509
582
|
}
|
|
510
583
|
}
|
|
511
584
|
|
|
512
585
|
const sqlExecution = this.buildSqlExecutionContext(method, tableName, logContext);
|
|
586
|
+
const sqlMethod = this.resolveSqlLogMethod(method, sqlExecution.sql);
|
|
513
587
|
const queryPromise = this.withConnection(async (conn) =>
|
|
514
|
-
this.executeQueryWithLifecycle(
|
|
588
|
+
this.executeQueryWithLifecycle(
|
|
589
|
+
conn,
|
|
590
|
+
method,
|
|
591
|
+
sqlExecution,
|
|
592
|
+
logContext,
|
|
593
|
+
sqlMethod
|
|
594
|
+
),
|
|
515
595
|
);
|
|
516
596
|
|
|
517
597
|
if (!cacheResults || !cacheRequestData || !requestArgumentsSerialized) {
|
|
518
598
|
return await queryPromise;
|
|
519
599
|
}
|
|
520
600
|
|
|
521
|
-
const cacheRequest = queryPromise.then((data) =>
|
|
522
|
-
|
|
523
|
-
|
|
601
|
+
const cacheRequest = queryPromise.then((data) => {
|
|
602
|
+
if (evictFromCache
|
|
603
|
+
&& data
|
|
604
|
+
&& typeof data === "object"
|
|
605
|
+
&& Array.isArray((data as DetermineResponseDataType<'GET', G['RestTableInterface']>).rest)) {
|
|
606
|
+
(data as DetermineResponseDataType<'GET', G['RestTableInterface']>).evictFromCache = evictFromCache;
|
|
607
|
+
}
|
|
608
|
+
return this.createCacheResponseEnvelope(method, tableName, data);
|
|
609
|
+
});
|
|
524
610
|
|
|
525
611
|
setCache(method, tableName, cacheRequestData, {
|
|
526
612
|
requestArgumentsSerialized,
|
|
@@ -640,6 +726,7 @@ export class SqlExecutor<
|
|
|
640
726
|
method: iRestMethods,
|
|
641
727
|
sqlExecution: iRestSqlExecutionContext,
|
|
642
728
|
logContext: ReturnType<typeof getLogContext>,
|
|
729
|
+
sqlMethod: string
|
|
643
730
|
): Promise<DetermineResponseDataType<G['RequestMethod'], G['RestTableInterface']>> {
|
|
644
731
|
const useTransaction = method !== C6C.GET;
|
|
645
732
|
let committed = false;
|
|
@@ -655,7 +742,27 @@ export class SqlExecutor<
|
|
|
655
742
|
await conn.beginTransaction();
|
|
656
743
|
}
|
|
657
744
|
|
|
658
|
-
|
|
745
|
+
let allowListStatus: SqlAllowListStatus = "not verified";
|
|
746
|
+
try {
|
|
747
|
+
allowListStatus = await this.validateSqlAllowList(sqlExecution.sql);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
logSql({
|
|
750
|
+
method: sqlMethod,
|
|
751
|
+
sql: sqlExecution.sql,
|
|
752
|
+
context: logContext,
|
|
753
|
+
cacheStatus: this.request.cacheResults === false ? "ignored" : "miss",
|
|
754
|
+
allowListStatus: "denied",
|
|
755
|
+
});
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
logSql({
|
|
760
|
+
method: sqlMethod,
|
|
761
|
+
sql: sqlExecution.sql,
|
|
762
|
+
context: logContext,
|
|
763
|
+
cacheStatus: this.request.cacheResults === false ? "ignored" : "miss",
|
|
764
|
+
allowListStatus,
|
|
765
|
+
});
|
|
659
766
|
|
|
660
767
|
await this.runLifecycleHooks<"beforeExecution">(
|
|
661
768
|
"beforeExecution",
|
|
@@ -729,17 +836,29 @@ export class SqlExecutor<
|
|
|
729
836
|
}
|
|
730
837
|
}
|
|
731
838
|
|
|
732
|
-
private async validateSqlAllowList(sql: string): Promise<
|
|
839
|
+
private async validateSqlAllowList(sql: string): Promise<SqlAllowListStatus> {
|
|
733
840
|
const allowListPath = this.config.sqlAllowListPath;
|
|
734
841
|
if (!allowListPath) {
|
|
735
|
-
return;
|
|
842
|
+
return "not verified";
|
|
736
843
|
}
|
|
737
844
|
|
|
738
845
|
const allowList = await loadSqlAllowList(allowListPath);
|
|
739
846
|
const normalized = normalizeSql(sql);
|
|
740
847
|
if (!allowList.has(normalized)) {
|
|
741
|
-
throw
|
|
848
|
+
throw createSqlAllowListBlockedError({
|
|
849
|
+
tableName:
|
|
850
|
+
typeof this.config.restModel?.TABLE_NAME === "string"
|
|
851
|
+
? this.config.restModel.TABLE_NAME
|
|
852
|
+
: undefined,
|
|
853
|
+
method:
|
|
854
|
+
typeof this.config.requestMethod === "string"
|
|
855
|
+
? this.config.requestMethod
|
|
856
|
+
: undefined,
|
|
857
|
+
normalizedSql: normalized,
|
|
858
|
+
allowListPath,
|
|
859
|
+
});
|
|
742
860
|
}
|
|
861
|
+
return "allowed";
|
|
743
862
|
}
|
|
744
863
|
|
|
745
864
|
|
|
@@ -2,8 +2,6 @@ import { OrmGenerics } from "../../types/ormGenerics";
|
|
|
2
2
|
import { SqlBuilderResult } from "../utils/sqlUtils";
|
|
3
3
|
import { JoinBuilder } from "../builders/JoinBuilder";
|
|
4
4
|
import { SelectQueryBuilder } from "./SelectQueryBuilder";
|
|
5
|
-
import logSql from "../../utils/logSql";
|
|
6
|
-
import {getLogContext} from "../../utils/logLevel";
|
|
7
5
|
|
|
8
6
|
export class DeleteQueryBuilder<G extends OrmGenerics> extends JoinBuilder<G> {
|
|
9
7
|
protected createSelectBuilder(request: any) {
|
|
@@ -27,8 +25,6 @@ export class DeleteQueryBuilder<G extends OrmGenerics> extends JoinBuilder<G> {
|
|
|
27
25
|
sql += this.buildWhereClause(this.request.WHERE, params);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
logSql("DELETE", sql, getLogContext(this.config, this.request));
|
|
31
|
-
|
|
32
28
|
return { sql, params };
|
|
33
29
|
}
|
|
34
30
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {C6C} from "../../constants/C6Constants";
|
|
2
2
|
import {ConditionBuilder} from "../builders/ConditionBuilder";
|
|
3
3
|
import {OrmGenerics} from "../../types/ormGenerics";
|
|
4
|
-
import logSql from "../../utils/logSql";
|
|
5
|
-
import {getLogContext} from "../../utils/logLevel";
|
|
6
4
|
|
|
7
5
|
export class PostQueryBuilder<G extends OrmGenerics> extends ConditionBuilder<G>{
|
|
8
6
|
|
|
@@ -61,8 +59,6 @@ export class PostQueryBuilder<G extends OrmGenerics> extends ConditionBuilder<G>
|
|
|
61
59
|
sql += ` ON DUPLICATE KEY UPDATE ${updateClause}`;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
logSql(verb, sql, getLogContext(this.config, this.request));
|
|
65
|
-
|
|
66
62
|
return {sql, params};
|
|
67
63
|
}
|
|
68
64
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {OrmGenerics} from "../../types/ormGenerics";
|
|
2
2
|
import {PaginationBuilder} from "../builders/PaginationBuilder";
|
|
3
3
|
import {SqlBuilderResult} from "../utils/sqlUtils";
|
|
4
|
-
import logSql from "../../utils/logSql";
|
|
5
|
-
import {getLogContext} from "../../utils/logLevel";
|
|
6
4
|
|
|
7
5
|
export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder<G>{
|
|
8
6
|
|
|
@@ -58,8 +56,6 @@ export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
|
|
|
58
56
|
sql += ` LIMIT 100`;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
logSql("SELECT", sql, getLogContext(this.config, this.request));
|
|
62
|
-
|
|
63
59
|
return { sql, params };
|
|
64
60
|
}
|
|
65
61
|
}
|
|
@@ -3,8 +3,6 @@ import {OrmGenerics} from "../../types/ormGenerics";
|
|
|
3
3
|
import { PaginationBuilder } from '../builders/PaginationBuilder';
|
|
4
4
|
import {SqlBuilderResult} from "../utils/sqlUtils";
|
|
5
5
|
import {SelectQueryBuilder} from "./SelectQueryBuilder";
|
|
6
|
-
import logSql from "../../utils/logSql";
|
|
7
|
-
import {getLogContext} from "../../utils/logLevel";
|
|
8
6
|
|
|
9
7
|
export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder<G>{
|
|
10
8
|
protected createSelectBuilder(request: any) {
|
|
@@ -56,8 +54,6 @@ export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
|
|
|
56
54
|
sql += this.buildPaginationClause(args.PAGINATION, params);
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
logSql("UPDATE", sql, getLogContext(this.config, this.request));
|
|
60
|
-
|
|
61
57
|
return { sql, params };
|
|
62
58
|
}
|
|
63
59
|
}
|
|
@@ -140,7 +140,10 @@ export type C6RestResponse<
|
|
|
140
140
|
session?: any;
|
|
141
141
|
sql?: any;
|
|
142
142
|
} & (Method extends 'GET'
|
|
143
|
-
? {
|
|
143
|
+
? {
|
|
144
|
+
next?: () => Promise<DetermineResponseDataType<'GET', RestData, Overrides>>,
|
|
145
|
+
evictFromCache?: () => boolean
|
|
146
|
+
}
|
|
144
147
|
: {
|
|
145
148
|
affected: number,
|
|
146
149
|
insertId?: number | string,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {iCacheAPI, iCacheResponse} from "../types/ormInterfaces";
|
|
2
|
-
import {LogLevel, logWithLevel, shouldLog} from "./logLevel";
|
|
2
|
+
import {LogContext, LogLevel, logWithLevel, shouldLog} from "./logLevel";
|
|
3
|
+
import logSql from "./logSql";
|
|
3
4
|
|
|
4
5
|
// -----------------------------------------------------------------------------
|
|
5
6
|
// Cache Storage
|
|
@@ -60,19 +61,26 @@ export function checkCache<ResponseDataType = any>(
|
|
|
60
61
|
method: string,
|
|
61
62
|
tableName: string | string[],
|
|
62
63
|
requestData: any,
|
|
64
|
+
logContext: LogContext,
|
|
63
65
|
): Promise<iCacheResponse<ResponseDataType>> | false {
|
|
64
66
|
const key = makeCacheKey(method, tableName, requestData);
|
|
65
67
|
const cached = apiRequestCache.get(key);
|
|
66
68
|
|
|
67
|
-
if (!cached)
|
|
69
|
+
if (!cached) {
|
|
70
|
+
console.log('apiRequestCache.size', apiRequestCache.size)
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
68
73
|
|
|
69
|
-
if (shouldLog(LogLevel.INFO,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
if (shouldLog(LogLevel.INFO, logContext)) {
|
|
75
|
+
const sql = cached.response?.data?.sql?.sql ?? "";
|
|
76
|
+
const sqlMethod = sql.trim().split(/\s+/, 1)[0]?.toUpperCase() || method;
|
|
77
|
+
logSql({
|
|
78
|
+
allowListStatus: "not verified",
|
|
79
|
+
cacheStatus: "hit",
|
|
80
|
+
context: logContext,
|
|
81
|
+
method: sqlMethod,
|
|
82
|
+
sql
|
|
83
|
+
});
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
return cached.request;
|
|
@@ -90,3 +98,12 @@ export function setCache<ResponseDataType = any>(
|
|
|
90
98
|
const key = makeCacheKey(method, tableName, requestData);
|
|
91
99
|
apiRequestCache.set(key, cacheEntry);
|
|
92
100
|
}
|
|
101
|
+
|
|
102
|
+
export function evictCacheEntry(
|
|
103
|
+
method: string,
|
|
104
|
+
tableName: string | string[],
|
|
105
|
+
requestData: any,
|
|
106
|
+
): boolean {
|
|
107
|
+
const key = makeCacheKey(method, tableName, requestData);
|
|
108
|
+
return apiRequestCache.delete(key);
|
|
109
|
+
}
|
package/src/utils/logLevel.ts
CHANGED
|
@@ -135,10 +135,9 @@ export const applyLogLevelDefaults = (
|
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
export const getLogContext = (
|
|
138
|
-
config
|
|
139
|
-
request
|
|
140
|
-
): LogContext
|
|
141
|
-
if (!config && !request) return undefined;
|
|
138
|
+
config: { logLevel?: number | null; verbose?: boolean | null },
|
|
139
|
+
request: { debug?: boolean } | null,
|
|
140
|
+
): LogContext => {
|
|
142
141
|
return {
|
|
143
142
|
logLevel: config?.logLevel ?? undefined,
|
|
144
143
|
verbose: config?.verbose ?? undefined,
|
package/src/utils/logSql.ts
CHANGED
|
@@ -2,19 +2,33 @@ import { getEnvBool } from "../variables/getEnv";
|
|
|
2
2
|
import colorSql from "./colorSql";
|
|
3
3
|
import { version } from "../../package.json";
|
|
4
4
|
import versionToRgb from "./versionColor";
|
|
5
|
-
import type {LogContext} from "./logLevel";
|
|
6
|
-
import {LogLevel, shouldLog} from "./logLevel";
|
|
5
|
+
import type { LogContext } from "./logLevel";
|
|
6
|
+
import { LogLevel, shouldLog } from "./logLevel";
|
|
7
|
+
|
|
8
|
+
export type SqlAllowListStatus = "allowed" | "denied" | "not verified";
|
|
9
|
+
export type SqlCacheStatus = "hit" | "miss" | "ignored";
|
|
10
|
+
|
|
11
|
+
export type LogSqlContextOptions = {
|
|
12
|
+
cacheStatus: SqlCacheStatus;
|
|
13
|
+
allowListStatus: SqlAllowListStatus;
|
|
14
|
+
method: string,
|
|
15
|
+
sql: string,
|
|
16
|
+
context?: LogContext,
|
|
17
|
+
};
|
|
7
18
|
|
|
8
19
|
const C = {
|
|
9
20
|
SSR: "\x1b[95m", // bright magenta
|
|
10
21
|
HTTP: "\x1b[94m", // bright blue
|
|
11
22
|
SQL: "\x1b[96m", // bright cyan
|
|
23
|
+
WARN: "\x1b[93m", // yellow
|
|
24
|
+
ORANGE: "\x1b[38;2;255;165;0m", // orange (truecolor)
|
|
25
|
+
ERROR: "\x1b[91m", // red
|
|
12
26
|
METHOD_COLORS: {
|
|
13
27
|
SELECT: "\x1b[92m", // green
|
|
14
28
|
INSERT: "\x1b[96m", // cyan
|
|
15
29
|
REPLACE: "\x1b[96m", // cyan
|
|
16
30
|
UPDATE: "\x1b[95m", // magenta
|
|
17
|
-
DELETE: "\x1b[
|
|
31
|
+
DELETE: "\x1b[38;2;255;179;179m", // very light red (truecolor)
|
|
18
32
|
},
|
|
19
33
|
METHOD_FALLBACK: [
|
|
20
34
|
"\x1b[92m", // green
|
|
@@ -24,6 +38,7 @@ const C = {
|
|
|
24
38
|
"\x1b[94m", // blue
|
|
25
39
|
"\x1b[97m", // white
|
|
26
40
|
],
|
|
41
|
+
GREY: "\x1b[90m", // light grey
|
|
27
42
|
RESET: "\x1b[0m",
|
|
28
43
|
};
|
|
29
44
|
|
|
@@ -47,13 +62,43 @@ function methodColor(method: string): string {
|
|
|
47
62
|
return C.METHOD_FALLBACK[idx];
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
const cacheLabel = (cacheStatus: SqlCacheStatus): string => {
|
|
66
|
+
switch (cacheStatus) {
|
|
67
|
+
case "hit":
|
|
68
|
+
return `${C.METHOD_COLORS.SELECT}[CACHE HIT]${C.RESET}`;
|
|
69
|
+
case "ignored":
|
|
70
|
+
return `${C.WARN}[CACHE IGNORED]${C.RESET}`;
|
|
71
|
+
default:
|
|
72
|
+
return `${C.ORANGE}[CACHE MISS]${C.RESET}`;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const allowListLabel = (status: SqlAllowListStatus): string => {
|
|
77
|
+
switch (status) {
|
|
78
|
+
case "allowed":
|
|
79
|
+
return `${C.METHOD_COLORS.SELECT}[VERIFIED]${C.RESET}`;
|
|
80
|
+
case "denied":
|
|
81
|
+
return `${C.ERROR}[DENIED]${C.RESET}`;
|
|
82
|
+
default:
|
|
83
|
+
return `${C.GREY}[NOT VERIFIED]${C.RESET}`;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default function logSql(
|
|
88
|
+
options: LogSqlContextOptions,
|
|
89
|
+
): void {
|
|
90
|
+
const method = options.method.toUpperCase();
|
|
91
|
+
|
|
92
|
+
if (!shouldLog(LogLevel.INFO, options.context)) return;
|
|
52
93
|
const preText = getEnvBool("SSR", false)
|
|
53
94
|
? `${C.SSR}[SSR]${C.RESET} `
|
|
54
95
|
: `${C.HTTP}[API]${C.RESET} `;
|
|
55
96
|
|
|
56
97
|
const labelColor = methodColor(method);
|
|
57
98
|
const versionColor = rgbAnsi(versionToRgb(version));
|
|
58
|
-
|
|
99
|
+
const cacheText = cacheLabel(options.cacheStatus);
|
|
100
|
+
const allowListText = allowListLabel(options.allowListStatus);
|
|
101
|
+
console.log(
|
|
102
|
+
`${versionColor}[${version}]${C.RESET} ${cacheText} ${allowListText} ${preText}${labelColor}[${method}]${C.RESET} ${colorSql(options.sql)}`,
|
|
103
|
+
);
|
|
59
104
|
}
|
|
@@ -1,9 +1,95 @@
|
|
|
1
1
|
import isNode from "../variables/isNode";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type AllowListCacheEntry = {
|
|
4
|
+
allowList: Set<string>;
|
|
5
|
+
mtimeMs: number;
|
|
6
|
+
size: number;
|
|
7
|
+
};
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const allowListCache = new Map<string, AllowListCacheEntry>();
|
|
10
|
+
|
|
11
|
+
const ANSI_ESCAPE_REGEX = /\x1b\[[0-9;]*m/g;
|
|
12
|
+
const COLLAPSED_BIND_ROW_REGEX = /\(\?\s*×\d+\)/g;
|
|
13
|
+
|
|
14
|
+
function collapseBindGroups(sql: string): string {
|
|
15
|
+
let normalized = sql.replace(
|
|
16
|
+
/\(\s*(\?(?:\s*,\s*\?)*)\s*\)/g,
|
|
17
|
+
(_match, binds: string) => {
|
|
18
|
+
const bindCount = (binds.match(/\?/g) ?? []).length;
|
|
19
|
+
return `(? ×${bindCount})`;
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
normalized = normalized.replace(
|
|
24
|
+
/(\(\?\s*×\d+\))(?:\s*,\s*\1)+/g,
|
|
25
|
+
(_match, row) => `${row} ×*`,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
normalized = normalized.replace(
|
|
29
|
+
/\b(VALUES|VALUE)\s+(\(\?\s*×\d+\))(?:\s*×\d+|\s*×\*)?/gi,
|
|
30
|
+
(_match, keyword: string, row: string) => `${keyword} ${row} ×*`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
normalized = normalized.replace(
|
|
34
|
+
/\bIN\s*\(\?\s*×\d+\)/gi,
|
|
35
|
+
"IN (? ×*)",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
normalized = normalized.replace(
|
|
39
|
+
/\(\?\s*×\d+\)\s*×\d+/g,
|
|
40
|
+
(match) => {
|
|
41
|
+
const row = match.match(COLLAPSED_BIND_ROW_REGEX)?.[0];
|
|
42
|
+
return row ? `${row} ×*` : match;
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeLimitOffset(sql: string): string {
|
|
50
|
+
return sql
|
|
51
|
+
.replace(/\bLIMIT\s+\d+\s*,\s*\d+\b/gi, "LIMIT ?, ?")
|
|
52
|
+
.replace(/\bLIMIT\s+\d+\s+OFFSET\s+\d+\b/gi, "LIMIT ? OFFSET ?")
|
|
53
|
+
.replace(/\bLIMIT\s+\d+\b/gi, "LIMIT ?")
|
|
54
|
+
.replace(/\bOFFSET\s+\d+\b/gi, "OFFSET ?");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeGeomFromTextLiterals(sql: string): string {
|
|
58
|
+
let normalized = sql.replace(
|
|
59
|
+
/ST_GEOMFROMTEXT\(\s*'POINT\([^']*\)'\s*,\s*(?:\d+|\?)\s*\)/gi,
|
|
60
|
+
"ST_GEOMFROMTEXT('POINT(? ?)', ?)",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
normalized = normalized.replace(
|
|
64
|
+
/ST_GEOMFROMTEXT\(\s*'POLYGON\(\([^']*\)\)'\s*,\s*(?:\d+|\?)\s*\)/gi,
|
|
65
|
+
"ST_GEOMFROMTEXT('POLYGON((?))', ?)",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeGeoFunctionNames(sql: string): string {
|
|
72
|
+
return sql
|
|
73
|
+
.replace(/\bST_DISTANCE_SPHERE\b/gi, "ST_DISTANCE_SPHERE")
|
|
74
|
+
.replace(/\bST_GEOMFROMTEXT\b/gi, "ST_GEOMFROMTEXT")
|
|
75
|
+
.replace(/\bMBRCONTAINS\b/gi, "MBRCONTAINS");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeTokenPunctuationSpacing(sql: string): string {
|
|
79
|
+
return sql.replace(/`,\s*`/g, "`, `");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const normalizeSql = (sql: string): string => {
|
|
83
|
+
let normalized = sql.replace(ANSI_ESCAPE_REGEX, " ");
|
|
84
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
85
|
+
normalized = normalizeGeoFunctionNames(normalized);
|
|
86
|
+
normalized = normalizeTokenPunctuationSpacing(normalized);
|
|
87
|
+
normalized = collapseBindGroups(normalized);
|
|
88
|
+
normalized = normalizeLimitOffset(normalized);
|
|
89
|
+
normalized = normalizeGeomFromTextLiterals(normalized);
|
|
90
|
+
normalized = normalized.replace(/;\s*$/, "");
|
|
91
|
+
return normalized.replace(/\s+/g, " ").trim();
|
|
92
|
+
};
|
|
7
93
|
|
|
8
94
|
const parseAllowList = (raw: string, sourcePath: string): string[] => {
|
|
9
95
|
let parsed: unknown;
|
|
@@ -30,15 +116,27 @@ const parseAllowList = (raw: string, sourcePath: string): string[] => {
|
|
|
30
116
|
};
|
|
31
117
|
|
|
32
118
|
export const loadSqlAllowList = async (allowListPath: string): Promise<Set<string>> => {
|
|
33
|
-
if (allowListCache.has(allowListPath)) {
|
|
34
|
-
return allowListCache.get(allowListPath)!;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
119
|
if (!isNode()) {
|
|
38
120
|
throw new Error("SQL allowlist validation requires a Node runtime.");
|
|
39
121
|
}
|
|
40
122
|
|
|
41
|
-
const {readFile} = await import("node:fs/promises");
|
|
123
|
+
const {readFile, stat} = await import("node:fs/promises");
|
|
124
|
+
|
|
125
|
+
let fileStat: { mtimeMs: number; size: number };
|
|
126
|
+
try {
|
|
127
|
+
fileStat = await stat(allowListPath);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(`SQL allowlist file not found at ${allowListPath}.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const cached = allowListCache.get(allowListPath);
|
|
133
|
+
if (
|
|
134
|
+
cached &&
|
|
135
|
+
cached.mtimeMs === fileStat.mtimeMs &&
|
|
136
|
+
cached.size === fileStat.size
|
|
137
|
+
) {
|
|
138
|
+
return cached.allowList;
|
|
139
|
+
}
|
|
42
140
|
|
|
43
141
|
let raw: string;
|
|
44
142
|
try {
|
|
@@ -49,7 +147,11 @@ export const loadSqlAllowList = async (allowListPath: string): Promise<Set<strin
|
|
|
49
147
|
|
|
50
148
|
const sqlEntries = parseAllowList(raw, allowListPath);
|
|
51
149
|
const allowList = new Set(sqlEntries);
|
|
52
|
-
allowListCache.set(allowListPath,
|
|
150
|
+
allowListCache.set(allowListPath, {
|
|
151
|
+
allowList,
|
|
152
|
+
mtimeMs: fileStat.mtimeMs,
|
|
153
|
+
size: fileStat.size,
|
|
154
|
+
});
|
|
53
155
|
return allowList;
|
|
54
156
|
};
|
|
55
157
|
|