catalyst-relay 0.2.1 → 0.2.2
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 +5 -4
- package/dist/index.d.mts +3 -23
- package/dist/index.d.ts +3 -23
- package/dist/index.js +36 -247
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -247
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -94,93 +94,6 @@ function dictToAbapXml(data, root = "DATA") {
|
|
|
94
94
|
</asx:abap>`;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// src/core/utils/sql.ts
|
|
98
|
-
var SqlValidationError = class extends Error {
|
|
99
|
-
constructor(message) {
|
|
100
|
-
super(message);
|
|
101
|
-
this.name = "SqlValidationError";
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
function validateSqlInput(input, maxLength = 1e4) {
|
|
105
|
-
if (typeof input !== "string") {
|
|
106
|
-
return err(new SqlValidationError("Input must be a string"));
|
|
107
|
-
}
|
|
108
|
-
if (input.length > maxLength) {
|
|
109
|
-
return err(new SqlValidationError(`Input exceeds maximum length of ${maxLength}`));
|
|
110
|
-
}
|
|
111
|
-
const dangerousPatterns = [
|
|
112
|
-
{
|
|
113
|
-
pattern: /\b(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|TRUNCATE)\s+/i,
|
|
114
|
-
description: "DDL/DML keywords (DROP, DELETE, INSERT, etc.)"
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
pattern: /;[\s]*\w/,
|
|
118
|
-
description: "Statement termination followed by another statement"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
pattern: /--[\s]*\w/,
|
|
122
|
-
description: "SQL comments with content"
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
pattern: /\/\*.*?\*\//,
|
|
126
|
-
description: "Block comments"
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
pattern: /\bEXEC(UTE)?\s*\(/i,
|
|
130
|
-
description: "Procedure execution"
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
pattern: /\bSP_\w+/i,
|
|
134
|
-
description: "Stored procedures"
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
pattern: /\bXP_\w+/i,
|
|
138
|
-
description: "Extended stored procedures"
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
pattern: /\bUNION\s+(ALL\s+)?SELECT/i,
|
|
142
|
-
description: "Union-based injection"
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
pattern: /@@\w+/,
|
|
146
|
-
description: "System variables"
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
pattern: /\bDECLARE\s+@/i,
|
|
150
|
-
description: "Variable declarations"
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
pattern: /\bCAST\s*\(/i,
|
|
154
|
-
description: "Type casting"
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
pattern: /\bCONVERT\s*\(/i,
|
|
158
|
-
description: "Type conversion"
|
|
159
|
-
}
|
|
160
|
-
];
|
|
161
|
-
for (const { pattern, description } of dangerousPatterns) {
|
|
162
|
-
if (pattern.test(input)) {
|
|
163
|
-
return err(new SqlValidationError(
|
|
164
|
-
`Input contains potentially dangerous SQL pattern: ${description}`
|
|
165
|
-
));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const specialCharMatches = input.match(/[;'"\\]/g);
|
|
169
|
-
const specialCharCount = specialCharMatches ? specialCharMatches.length : 0;
|
|
170
|
-
if (specialCharCount > 5) {
|
|
171
|
-
return err(new SqlValidationError("Input contains excessive special characters"));
|
|
172
|
-
}
|
|
173
|
-
const singleQuoteCount = (input.match(/'/g) || []).length;
|
|
174
|
-
if (singleQuoteCount % 2 !== 0) {
|
|
175
|
-
return err(new SqlValidationError("Unbalanced single quotes detected"));
|
|
176
|
-
}
|
|
177
|
-
const doubleQuoteCount = (input.match(/"/g) || []).length;
|
|
178
|
-
if (doubleQuoteCount % 2 !== 0) {
|
|
179
|
-
return err(new SqlValidationError("Unbalanced double quotes detected"));
|
|
180
|
-
}
|
|
181
|
-
return ok(true);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
97
|
// src/core/utils/csrf.ts
|
|
185
98
|
var FETCH_CSRF_TOKEN = "fetch";
|
|
186
99
|
var CSRF_TOKEN_HEADER = "x-csrf-token";
|
|
@@ -927,63 +840,6 @@ function extractTransports(xml) {
|
|
|
927
840
|
return ok(transports);
|
|
928
841
|
}
|
|
929
842
|
|
|
930
|
-
// src/core/adt/queryBuilder.ts
|
|
931
|
-
function buildWhereClauses(filters) {
|
|
932
|
-
if (!filters || filters.length === 0) {
|
|
933
|
-
return "";
|
|
934
|
-
}
|
|
935
|
-
const clauses = filters.map((filter) => {
|
|
936
|
-
const { column, operator, value } = filter;
|
|
937
|
-
switch (operator) {
|
|
938
|
-
case "eq":
|
|
939
|
-
return `${column} = ${formatValue(value)}`;
|
|
940
|
-
case "ne":
|
|
941
|
-
return `${column} != ${formatValue(value)}`;
|
|
942
|
-
case "gt":
|
|
943
|
-
return `${column} > ${formatValue(value)}`;
|
|
944
|
-
case "ge":
|
|
945
|
-
return `${column} >= ${formatValue(value)}`;
|
|
946
|
-
case "lt":
|
|
947
|
-
return `${column} < ${formatValue(value)}`;
|
|
948
|
-
case "le":
|
|
949
|
-
return `${column} <= ${formatValue(value)}`;
|
|
950
|
-
case "like":
|
|
951
|
-
return `${column} LIKE ${formatValue(value)}`;
|
|
952
|
-
case "in":
|
|
953
|
-
if (Array.isArray(value)) {
|
|
954
|
-
const values = value.map((v) => formatValue(v)).join(", ");
|
|
955
|
-
return `${column} IN (${values})`;
|
|
956
|
-
}
|
|
957
|
-
return `${column} IN (${formatValue(value)})`;
|
|
958
|
-
default:
|
|
959
|
-
return "";
|
|
960
|
-
}
|
|
961
|
-
}).filter((c) => c);
|
|
962
|
-
if (clauses.length === 0) {
|
|
963
|
-
return "";
|
|
964
|
-
}
|
|
965
|
-
return ` WHERE ${clauses.join(" AND ")}`;
|
|
966
|
-
}
|
|
967
|
-
function buildOrderByClauses(orderBy) {
|
|
968
|
-
if (!orderBy || orderBy.length === 0) {
|
|
969
|
-
return "";
|
|
970
|
-
}
|
|
971
|
-
const clauses = orderBy.map((o) => `${o.column} ${o.direction.toUpperCase()}`);
|
|
972
|
-
return ` ORDER BY ${clauses.join(", ")}`;
|
|
973
|
-
}
|
|
974
|
-
function formatValue(value) {
|
|
975
|
-
if (value === null) {
|
|
976
|
-
return "NULL";
|
|
977
|
-
}
|
|
978
|
-
if (typeof value === "string") {
|
|
979
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
980
|
-
}
|
|
981
|
-
if (typeof value === "boolean") {
|
|
982
|
-
return value ? "1" : "0";
|
|
983
|
-
}
|
|
984
|
-
return String(value);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
843
|
// src/core/adt/previewParser.ts
|
|
988
844
|
function parseDataPreview(xml, maxRows, isTable) {
|
|
989
845
|
const [doc, parseErr] = safeParseXml(xml);
|
|
@@ -1002,10 +858,18 @@ function parseDataPreview(xml, maxRows, isTable) {
|
|
|
1002
858
|
if (!name || !dataType) continue;
|
|
1003
859
|
columns.push({ name, dataType });
|
|
1004
860
|
}
|
|
861
|
+
const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
|
|
862
|
+
if (columns.length === 0 && dataSetElements.length > 0) {
|
|
863
|
+
for (let i = 0; i < dataSetElements.length; i++) {
|
|
864
|
+
const dataSet = dataSetElements[i];
|
|
865
|
+
if (!dataSet) continue;
|
|
866
|
+
const name = dataSet.getAttributeNS(namespace, "columnName") || dataSet.getAttribute("columnName") || `column${i}`;
|
|
867
|
+
columns.push({ name, dataType: "unknown" });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
1005
870
|
if (columns.length === 0) {
|
|
1006
|
-
return
|
|
871
|
+
return ok({ columns: [], rows: [], totalRows: 0 });
|
|
1007
872
|
}
|
|
1008
|
-
const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
|
|
1009
873
|
const columnData = Array.from({ length: columns.length }, () => []);
|
|
1010
874
|
for (let i = 0; i < dataSetElements.length; i++) {
|
|
1011
875
|
const dataSet = dataSetElements[i];
|
|
@@ -1035,23 +899,16 @@ function parseDataPreview(xml, maxRows, isTable) {
|
|
|
1035
899
|
return ok(dataFrame);
|
|
1036
900
|
}
|
|
1037
901
|
|
|
1038
|
-
// src/core/adt/
|
|
902
|
+
// src/core/adt/dataPreview.ts
|
|
1039
903
|
async function previewData(client, query) {
|
|
1040
904
|
const extension = query.objectType === "table" ? "astabldt" : "asddls";
|
|
1041
905
|
const config = getConfigByExtension(extension);
|
|
1042
|
-
if (!config
|
|
906
|
+
if (!config?.dpEndpoint || !config?.dpParam) {
|
|
1043
907
|
return err(new Error(`Data preview not supported for object type: ${query.objectType}`));
|
|
1044
908
|
}
|
|
1045
909
|
const limit = query.limit ?? 100;
|
|
1046
|
-
const whereClauses = buildWhereClauses(query.filters);
|
|
1047
|
-
const orderByClauses = buildOrderByClauses(query.orderBy);
|
|
1048
|
-
const sqlQuery = `select * from ${query.objectName}${whereClauses}${orderByClauses}`;
|
|
1049
|
-
const [, validationErr] = validateSqlInput(sqlQuery);
|
|
1050
|
-
if (validationErr) {
|
|
1051
|
-
return err(new Error(`SQL validation failed: ${validationErr.message}`));
|
|
1052
|
-
}
|
|
1053
910
|
debug(`Data preview: endpoint=${config.dpEndpoint}, param=${config.dpParam}=${query.objectName}`);
|
|
1054
|
-
debug(`SQL: ${sqlQuery}`);
|
|
911
|
+
debug(`SQL: ${query.sqlQuery}`);
|
|
1055
912
|
const [response, requestErr] = await client.request({
|
|
1056
913
|
method: "POST",
|
|
1057
914
|
path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
|
|
@@ -1063,7 +920,7 @@ async function previewData(client, query) {
|
|
|
1063
920
|
"Accept": "application/vnd.sap.adt.datapreview.table.v1+xml",
|
|
1064
921
|
"Content-Type": "text/plain"
|
|
1065
922
|
},
|
|
1066
|
-
body: sqlQuery
|
|
923
|
+
body: query.sqlQuery
|
|
1067
924
|
});
|
|
1068
925
|
if (requestErr) {
|
|
1069
926
|
return err(requestErr);
|
|
@@ -1085,109 +942,41 @@ async function previewData(client, query) {
|
|
|
1085
942
|
// src/core/adt/distinct.ts
|
|
1086
943
|
var MAX_ROW_COUNT = 5e4;
|
|
1087
944
|
async function getDistinctValues(client, objectName, column, objectType = "view") {
|
|
1088
|
-
const extension = objectType === "table" ? "astabldt" : "asddls";
|
|
1089
|
-
const config = getConfigByExtension(extension);
|
|
1090
|
-
if (!config || !config.dpEndpoint || !config.dpParam) {
|
|
1091
|
-
return err(new Error(`Data preview not supported for object type: ${objectType}`));
|
|
1092
|
-
}
|
|
1093
945
|
const columnName = column.toUpperCase();
|
|
1094
946
|
const sqlQuery = `SELECT ${columnName} AS value, COUNT(*) AS count FROM ${objectName} GROUP BY ${columnName}`;
|
|
1095
|
-
const [,
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
method: "POST",
|
|
1101
|
-
path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
|
|
1102
|
-
params: {
|
|
1103
|
-
"rowNumber": MAX_ROW_COUNT,
|
|
1104
|
-
[config.dpParam]: objectName
|
|
1105
|
-
},
|
|
1106
|
-
headers: {
|
|
1107
|
-
"Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
|
|
1108
|
-
},
|
|
1109
|
-
body: sqlQuery
|
|
947
|
+
const [dataFrame, error] = await previewData(client, {
|
|
948
|
+
objectName,
|
|
949
|
+
objectType,
|
|
950
|
+
sqlQuery,
|
|
951
|
+
limit: MAX_ROW_COUNT
|
|
1110
952
|
});
|
|
1111
|
-
if (
|
|
1112
|
-
return err(
|
|
953
|
+
if (error) {
|
|
954
|
+
return err(new Error(`Distinct values query failed: ${error.message}`));
|
|
1113
955
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1119
|
-
const text = await response.text();
|
|
1120
|
-
const [doc, parseErr] = safeParseXml(text);
|
|
1121
|
-
if (parseErr) {
|
|
1122
|
-
return err(parseErr);
|
|
1123
|
-
}
|
|
1124
|
-
const dataSets = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "dataSet");
|
|
1125
|
-
const values = [];
|
|
1126
|
-
for (let i = 0; i < dataSets.length; i++) {
|
|
1127
|
-
const dataSet = dataSets[i];
|
|
1128
|
-
if (!dataSet) continue;
|
|
1129
|
-
const dataElements = dataSet.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
|
|
1130
|
-
if (dataElements.length < 2) continue;
|
|
1131
|
-
const value = dataElements[0]?.textContent ?? "";
|
|
1132
|
-
const countText = dataElements[1]?.textContent?.trim() ?? "0";
|
|
1133
|
-
values.push({
|
|
1134
|
-
value,
|
|
1135
|
-
count: parseInt(countText, 10)
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
const result = {
|
|
1139
|
-
column,
|
|
1140
|
-
values
|
|
1141
|
-
};
|
|
1142
|
-
return ok(result);
|
|
956
|
+
const values = dataFrame.rows.map((row) => ({
|
|
957
|
+
value: row[0],
|
|
958
|
+
count: parseInt(String(row[1]), 10)
|
|
959
|
+
}));
|
|
960
|
+
return ok({ column, values });
|
|
1143
961
|
}
|
|
1144
962
|
|
|
1145
963
|
// src/core/adt/count.ts
|
|
1146
964
|
async function countRows(client, objectName, objectType) {
|
|
1147
|
-
const extension = objectType === "table" ? "astabldt" : "asddls";
|
|
1148
|
-
const config = getConfigByExtension(extension);
|
|
1149
|
-
if (!config || !config.dpEndpoint || !config.dpParam) {
|
|
1150
|
-
return err(new Error(`Data preview not supported for object type: ${objectType}`));
|
|
1151
|
-
}
|
|
1152
965
|
const sqlQuery = `SELECT COUNT(*) AS count FROM ${objectName}`;
|
|
1153
|
-
const [,
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
method: "POST",
|
|
1159
|
-
path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
|
|
1160
|
-
params: {
|
|
1161
|
-
"rowNumber": 1,
|
|
1162
|
-
[config.dpParam]: objectName
|
|
1163
|
-
},
|
|
1164
|
-
headers: {
|
|
1165
|
-
"Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
|
|
1166
|
-
},
|
|
1167
|
-
body: sqlQuery
|
|
966
|
+
const [dataFrame, error] = await previewData(client, {
|
|
967
|
+
objectName,
|
|
968
|
+
objectType,
|
|
969
|
+
sqlQuery,
|
|
970
|
+
limit: 1
|
|
1168
971
|
});
|
|
1169
|
-
if (
|
|
1170
|
-
return err(
|
|
1171
|
-
}
|
|
1172
|
-
if (!response.ok) {
|
|
1173
|
-
const text2 = await response.text();
|
|
1174
|
-
const errorMsg = extractError(text2);
|
|
1175
|
-
return err(new Error(`Row count query failed: ${errorMsg}`));
|
|
972
|
+
if (error) {
|
|
973
|
+
return err(new Error(`Row count query failed: ${error.message}`));
|
|
1176
974
|
}
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
if (parseErr) {
|
|
1180
|
-
return err(parseErr);
|
|
1181
|
-
}
|
|
1182
|
-
const dataElements = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
|
|
1183
|
-
if (dataElements.length === 0) {
|
|
975
|
+
const countValue = dataFrame.rows[0]?.[0];
|
|
976
|
+
if (countValue === void 0) {
|
|
1184
977
|
return err(new Error("No count value returned"));
|
|
1185
978
|
}
|
|
1186
|
-
const
|
|
1187
|
-
if (!countText) {
|
|
1188
|
-
return err(new Error("Empty count value returned"));
|
|
1189
|
-
}
|
|
1190
|
-
const count = parseInt(countText, 10);
|
|
979
|
+
const count = parseInt(String(countValue), 10);
|
|
1191
980
|
if (isNaN(count)) {
|
|
1192
981
|
return err(new Error("Invalid count value returned"));
|
|
1193
982
|
}
|