@carbonorm/carbonnode 6.0.13 → 6.0.17
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 +413 -245
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +413 -245
- package/dist/index.esm.js.map +1 -1
- package/dist/utils/cacheManager.d.ts +2 -1
- package/dist/utils/logLevel.d.ts +3 -3
- package/dist/utils/logSql.d.ts +10 -1
- package/package.json +1 -1
- package/scripts/assets/handlebars/C6.ts.handlebars +1 -1
- package/src/__tests__/fixtures/sqlResponses/sqlAllowList.json +1 -1
- package/src/__tests__/httpExecutor.multiRowUpsert.test.ts +50 -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/executors/HttpExecutor.ts +7 -2
- package/src/executors/SqlExecutor.ts +108 -7
- 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/utils/cacheManager.ts +17 -9
- package/src/utils/logLevel.ts +3 -4
- package/src/utils/logSql.ts +51 -6
- package/src/utils/sqlAllowList.ts +111 -9
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
|
|