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/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 err(new Error("No columns found in preview response"));
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/data.ts
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 || !config.dpEndpoint || !config.dpParam) {
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 [, validationErr] = validateSqlInput(sqlQuery);
1096
- if (validationErr) {
1097
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1098
- }
1099
- const [response, requestErr] = await client.request({
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 (requestErr) {
1112
- return err(requestErr);
953
+ if (error) {
954
+ return err(new Error(`Distinct values query failed: ${error.message}`));
1113
955
  }
1114
- if (!response.ok) {
1115
- const text2 = await response.text();
1116
- const errorMsg = extractError(text2);
1117
- return err(new Error(`Distinct values query failed: ${errorMsg}`));
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 [, validationErr] = validateSqlInput(sqlQuery);
1154
- if (validationErr) {
1155
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1156
- }
1157
- const [response, requestErr] = await client.request({
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 (requestErr) {
1170
- return err(requestErr);
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 text = await response.text();
1178
- const [doc, parseErr] = safeParseXml(text);
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 countText = dataElements[0]?.textContent?.trim();
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
  }