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 CHANGED
@@ -270,9 +270,9 @@ if (!error) {
270
270
  ```typescript
271
271
  const [data, error] = await client.previewData({
272
272
  objectName: 'T000',
273
- columns: ['MANDT', 'MTEXT'],
274
- limit: 10,
275
- where: "MANDT = '100'"
273
+ objectType: 'table',
274
+ sqlQuery: "SELECT MANDT, MTEXT FROM T000 WHERE MANDT = '100'",
275
+ limit: 10
276
276
  });
277
277
  ```
278
278
 
@@ -327,7 +327,8 @@ curl -X POST http://localhost:3000/preview/data \
327
327
  -H "x-session-id: abc123" \
328
328
  -d '{
329
329
  "objectName": "T000",
330
- "columns": ["MANDT", "MTEXT"],
330
+ "objectType": "table",
331
+ "sqlQuery": "SELECT MANDT, MTEXT FROM T000 WHERE MANDT = '\''100'\''",
331
332
  "limit": 10
332
333
  }'
333
334
  ```
package/dist/index.d.mts CHANGED
@@ -113,30 +113,10 @@ interface PreviewQuery {
113
113
  objectName: string;
114
114
  /** Object type ('table' or 'view') */
115
115
  objectType: 'table' | 'view';
116
- /** WHERE clause filters */
117
- filters?: Filter[];
118
- /** ORDER BY columns */
119
- orderBy?: OrderBy[];
116
+ /** SQL query to execute */
117
+ sqlQuery: string;
120
118
  /** Maximum rows to return (default: 100) */
121
119
  limit?: number;
122
- /** Row offset for pagination */
123
- offset?: number;
124
- }
125
- /**
126
- * Filter condition for data preview
127
- */
128
- interface Filter {
129
- column: string;
130
- operator: FilterOperator;
131
- value: string | number | boolean | null;
132
- }
133
- type FilterOperator = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le' | 'like' | 'in';
134
- /**
135
- * Sort specification for data preview
136
- */
137
- interface OrderBy {
138
- column: string;
139
- direction: 'asc' | 'desc';
140
120
  }
141
121
 
142
122
  /**
@@ -464,4 +444,4 @@ interface ADTClient {
464
444
  }
465
445
  declare function createClient(config: ClientConfig): Result<ADTClient, Error>;
466
446
 
467
- export { type ADTClient, type ActivationMessage, type ActivationResult, type ApiResponse, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type ClientConfig, type ColumnInfo, type DataFrame, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type Filter, type FilterOperator, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectRef, type ObjectWithContent, type OrderBy, type Package, type PreviewQuery, type Result, type SamlAuthConfig, type SearchResult, type Session, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeNode, type TreeQuery, type UpsertResult, createClient, err, ok };
447
+ export { type ADTClient, type ActivationMessage, type ActivationResult, type ApiResponse, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type ClientConfig, type ColumnInfo, type DataFrame, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectRef, type ObjectWithContent, type Package, type PreviewQuery, type Result, type SamlAuthConfig, type SearchResult, type Session, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeNode, type TreeQuery, type UpsertResult, createClient, err, ok };
package/dist/index.d.ts CHANGED
@@ -113,30 +113,10 @@ interface PreviewQuery {
113
113
  objectName: string;
114
114
  /** Object type ('table' or 'view') */
115
115
  objectType: 'table' | 'view';
116
- /** WHERE clause filters */
117
- filters?: Filter[];
118
- /** ORDER BY columns */
119
- orderBy?: OrderBy[];
116
+ /** SQL query to execute */
117
+ sqlQuery: string;
120
118
  /** Maximum rows to return (default: 100) */
121
119
  limit?: number;
122
- /** Row offset for pagination */
123
- offset?: number;
124
- }
125
- /**
126
- * Filter condition for data preview
127
- */
128
- interface Filter {
129
- column: string;
130
- operator: FilterOperator;
131
- value: string | number | boolean | null;
132
- }
133
- type FilterOperator = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le' | 'like' | 'in';
134
- /**
135
- * Sort specification for data preview
136
- */
137
- interface OrderBy {
138
- column: string;
139
- direction: 'asc' | 'desc';
140
120
  }
141
121
 
142
122
  /**
@@ -464,4 +444,4 @@ interface ADTClient {
464
444
  }
465
445
  declare function createClient(config: ClientConfig): Result<ADTClient, Error>;
466
446
 
467
- export { type ADTClient, type ActivationMessage, type ActivationResult, type ApiResponse, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type ClientConfig, type ColumnInfo, type DataFrame, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type Filter, type FilterOperator, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectRef, type ObjectWithContent, type OrderBy, type Package, type PreviewQuery, type Result, type SamlAuthConfig, type SearchResult, type Session, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeNode, type TreeQuery, type UpsertResult, createClient, err, ok };
447
+ export { type ADTClient, type ActivationMessage, type ActivationResult, type ApiResponse, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type ClientConfig, type ColumnInfo, type DataFrame, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectRef, type ObjectWithContent, type Package, type PreviewQuery, type Result, type SamlAuthConfig, type SearchResult, type Session, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeNode, type TreeQuery, type UpsertResult, createClient, err, ok };
package/dist/index.js CHANGED
@@ -125,93 +125,6 @@ function dictToAbapXml(data, root = "DATA") {
125
125
  </asx:abap>`;
126
126
  }
127
127
 
128
- // src/core/utils/sql.ts
129
- var SqlValidationError = class extends Error {
130
- constructor(message) {
131
- super(message);
132
- this.name = "SqlValidationError";
133
- }
134
- };
135
- function validateSqlInput(input, maxLength = 1e4) {
136
- if (typeof input !== "string") {
137
- return err(new SqlValidationError("Input must be a string"));
138
- }
139
- if (input.length > maxLength) {
140
- return err(new SqlValidationError(`Input exceeds maximum length of ${maxLength}`));
141
- }
142
- const dangerousPatterns = [
143
- {
144
- pattern: /\b(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|TRUNCATE)\s+/i,
145
- description: "DDL/DML keywords (DROP, DELETE, INSERT, etc.)"
146
- },
147
- {
148
- pattern: /;[\s]*\w/,
149
- description: "Statement termination followed by another statement"
150
- },
151
- {
152
- pattern: /--[\s]*\w/,
153
- description: "SQL comments with content"
154
- },
155
- {
156
- pattern: /\/\*.*?\*\//,
157
- description: "Block comments"
158
- },
159
- {
160
- pattern: /\bEXEC(UTE)?\s*\(/i,
161
- description: "Procedure execution"
162
- },
163
- {
164
- pattern: /\bSP_\w+/i,
165
- description: "Stored procedures"
166
- },
167
- {
168
- pattern: /\bXP_\w+/i,
169
- description: "Extended stored procedures"
170
- },
171
- {
172
- pattern: /\bUNION\s+(ALL\s+)?SELECT/i,
173
- description: "Union-based injection"
174
- },
175
- {
176
- pattern: /@@\w+/,
177
- description: "System variables"
178
- },
179
- {
180
- pattern: /\bDECLARE\s+@/i,
181
- description: "Variable declarations"
182
- },
183
- {
184
- pattern: /\bCAST\s*\(/i,
185
- description: "Type casting"
186
- },
187
- {
188
- pattern: /\bCONVERT\s*\(/i,
189
- description: "Type conversion"
190
- }
191
- ];
192
- for (const { pattern, description } of dangerousPatterns) {
193
- if (pattern.test(input)) {
194
- return err(new SqlValidationError(
195
- `Input contains potentially dangerous SQL pattern: ${description}`
196
- ));
197
- }
198
- }
199
- const specialCharMatches = input.match(/[;'"\\]/g);
200
- const specialCharCount = specialCharMatches ? specialCharMatches.length : 0;
201
- if (specialCharCount > 5) {
202
- return err(new SqlValidationError("Input contains excessive special characters"));
203
- }
204
- const singleQuoteCount = (input.match(/'/g) || []).length;
205
- if (singleQuoteCount % 2 !== 0) {
206
- return err(new SqlValidationError("Unbalanced single quotes detected"));
207
- }
208
- const doubleQuoteCount = (input.match(/"/g) || []).length;
209
- if (doubleQuoteCount % 2 !== 0) {
210
- return err(new SqlValidationError("Unbalanced double quotes detected"));
211
- }
212
- return ok(true);
213
- }
214
-
215
128
  // src/core/utils/csrf.ts
216
129
  var FETCH_CSRF_TOKEN = "fetch";
217
130
  var CSRF_TOKEN_HEADER = "x-csrf-token";
@@ -958,63 +871,6 @@ function extractTransports(xml) {
958
871
  return ok(transports);
959
872
  }
960
873
 
961
- // src/core/adt/queryBuilder.ts
962
- function buildWhereClauses(filters) {
963
- if (!filters || filters.length === 0) {
964
- return "";
965
- }
966
- const clauses = filters.map((filter) => {
967
- const { column, operator, value } = filter;
968
- switch (operator) {
969
- case "eq":
970
- return `${column} = ${formatValue(value)}`;
971
- case "ne":
972
- return `${column} != ${formatValue(value)}`;
973
- case "gt":
974
- return `${column} > ${formatValue(value)}`;
975
- case "ge":
976
- return `${column} >= ${formatValue(value)}`;
977
- case "lt":
978
- return `${column} < ${formatValue(value)}`;
979
- case "le":
980
- return `${column} <= ${formatValue(value)}`;
981
- case "like":
982
- return `${column} LIKE ${formatValue(value)}`;
983
- case "in":
984
- if (Array.isArray(value)) {
985
- const values = value.map((v) => formatValue(v)).join(", ");
986
- return `${column} IN (${values})`;
987
- }
988
- return `${column} IN (${formatValue(value)})`;
989
- default:
990
- return "";
991
- }
992
- }).filter((c) => c);
993
- if (clauses.length === 0) {
994
- return "";
995
- }
996
- return ` WHERE ${clauses.join(" AND ")}`;
997
- }
998
- function buildOrderByClauses(orderBy) {
999
- if (!orderBy || orderBy.length === 0) {
1000
- return "";
1001
- }
1002
- const clauses = orderBy.map((o) => `${o.column} ${o.direction.toUpperCase()}`);
1003
- return ` ORDER BY ${clauses.join(", ")}`;
1004
- }
1005
- function formatValue(value) {
1006
- if (value === null) {
1007
- return "NULL";
1008
- }
1009
- if (typeof value === "string") {
1010
- return `'${value.replace(/'/g, "''")}'`;
1011
- }
1012
- if (typeof value === "boolean") {
1013
- return value ? "1" : "0";
1014
- }
1015
- return String(value);
1016
- }
1017
-
1018
874
  // src/core/adt/previewParser.ts
1019
875
  function parseDataPreview(xml, maxRows, isTable) {
1020
876
  const [doc, parseErr] = safeParseXml(xml);
@@ -1033,10 +889,18 @@ function parseDataPreview(xml, maxRows, isTable) {
1033
889
  if (!name || !dataType) continue;
1034
890
  columns.push({ name, dataType });
1035
891
  }
892
+ const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
893
+ if (columns.length === 0 && dataSetElements.length > 0) {
894
+ for (let i = 0; i < dataSetElements.length; i++) {
895
+ const dataSet = dataSetElements[i];
896
+ if (!dataSet) continue;
897
+ const name = dataSet.getAttributeNS(namespace, "columnName") || dataSet.getAttribute("columnName") || `column${i}`;
898
+ columns.push({ name, dataType: "unknown" });
899
+ }
900
+ }
1036
901
  if (columns.length === 0) {
1037
- return err(new Error("No columns found in preview response"));
902
+ return ok({ columns: [], rows: [], totalRows: 0 });
1038
903
  }
1039
- const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
1040
904
  const columnData = Array.from({ length: columns.length }, () => []);
1041
905
  for (let i = 0; i < dataSetElements.length; i++) {
1042
906
  const dataSet = dataSetElements[i];
@@ -1066,23 +930,16 @@ function parseDataPreview(xml, maxRows, isTable) {
1066
930
  return ok(dataFrame);
1067
931
  }
1068
932
 
1069
- // src/core/adt/data.ts
933
+ // src/core/adt/dataPreview.ts
1070
934
  async function previewData(client, query) {
1071
935
  const extension = query.objectType === "table" ? "astabldt" : "asddls";
1072
936
  const config = getConfigByExtension(extension);
1073
- if (!config || !config.dpEndpoint || !config.dpParam) {
937
+ if (!config?.dpEndpoint || !config?.dpParam) {
1074
938
  return err(new Error(`Data preview not supported for object type: ${query.objectType}`));
1075
939
  }
1076
940
  const limit = query.limit ?? 100;
1077
- const whereClauses = buildWhereClauses(query.filters);
1078
- const orderByClauses = buildOrderByClauses(query.orderBy);
1079
- const sqlQuery = `select * from ${query.objectName}${whereClauses}${orderByClauses}`;
1080
- const [, validationErr] = validateSqlInput(sqlQuery);
1081
- if (validationErr) {
1082
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1083
- }
1084
941
  debug(`Data preview: endpoint=${config.dpEndpoint}, param=${config.dpParam}=${query.objectName}`);
1085
- debug(`SQL: ${sqlQuery}`);
942
+ debug(`SQL: ${query.sqlQuery}`);
1086
943
  const [response, requestErr] = await client.request({
1087
944
  method: "POST",
1088
945
  path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
@@ -1094,7 +951,7 @@ async function previewData(client, query) {
1094
951
  "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml",
1095
952
  "Content-Type": "text/plain"
1096
953
  },
1097
- body: sqlQuery
954
+ body: query.sqlQuery
1098
955
  });
1099
956
  if (requestErr) {
1100
957
  return err(requestErr);
@@ -1116,109 +973,41 @@ async function previewData(client, query) {
1116
973
  // src/core/adt/distinct.ts
1117
974
  var MAX_ROW_COUNT = 5e4;
1118
975
  async function getDistinctValues(client, objectName, column, objectType = "view") {
1119
- const extension = objectType === "table" ? "astabldt" : "asddls";
1120
- const config = getConfigByExtension(extension);
1121
- if (!config || !config.dpEndpoint || !config.dpParam) {
1122
- return err(new Error(`Data preview not supported for object type: ${objectType}`));
1123
- }
1124
976
  const columnName = column.toUpperCase();
1125
977
  const sqlQuery = `SELECT ${columnName} AS value, COUNT(*) AS count FROM ${objectName} GROUP BY ${columnName}`;
1126
- const [, validationErr] = validateSqlInput(sqlQuery);
1127
- if (validationErr) {
1128
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1129
- }
1130
- const [response, requestErr] = await client.request({
1131
- method: "POST",
1132
- path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
1133
- params: {
1134
- "rowNumber": MAX_ROW_COUNT,
1135
- [config.dpParam]: objectName
1136
- },
1137
- headers: {
1138
- "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
1139
- },
1140
- body: sqlQuery
978
+ const [dataFrame, error] = await previewData(client, {
979
+ objectName,
980
+ objectType,
981
+ sqlQuery,
982
+ limit: MAX_ROW_COUNT
1141
983
  });
1142
- if (requestErr) {
1143
- return err(requestErr);
984
+ if (error) {
985
+ return err(new Error(`Distinct values query failed: ${error.message}`));
1144
986
  }
1145
- if (!response.ok) {
1146
- const text2 = await response.text();
1147
- const errorMsg = extractError(text2);
1148
- return err(new Error(`Distinct values query failed: ${errorMsg}`));
1149
- }
1150
- const text = await response.text();
1151
- const [doc, parseErr] = safeParseXml(text);
1152
- if (parseErr) {
1153
- return err(parseErr);
1154
- }
1155
- const dataSets = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "dataSet");
1156
- const values = [];
1157
- for (let i = 0; i < dataSets.length; i++) {
1158
- const dataSet = dataSets[i];
1159
- if (!dataSet) continue;
1160
- const dataElements = dataSet.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
1161
- if (dataElements.length < 2) continue;
1162
- const value = dataElements[0]?.textContent ?? "";
1163
- const countText = dataElements[1]?.textContent?.trim() ?? "0";
1164
- values.push({
1165
- value,
1166
- count: parseInt(countText, 10)
1167
- });
1168
- }
1169
- const result = {
1170
- column,
1171
- values
1172
- };
1173
- return ok(result);
987
+ const values = dataFrame.rows.map((row) => ({
988
+ value: row[0],
989
+ count: parseInt(String(row[1]), 10)
990
+ }));
991
+ return ok({ column, values });
1174
992
  }
1175
993
 
1176
994
  // src/core/adt/count.ts
1177
995
  async function countRows(client, objectName, objectType) {
1178
- const extension = objectType === "table" ? "astabldt" : "asddls";
1179
- const config = getConfigByExtension(extension);
1180
- if (!config || !config.dpEndpoint || !config.dpParam) {
1181
- return err(new Error(`Data preview not supported for object type: ${objectType}`));
1182
- }
1183
996
  const sqlQuery = `SELECT COUNT(*) AS count FROM ${objectName}`;
1184
- const [, validationErr] = validateSqlInput(sqlQuery);
1185
- if (validationErr) {
1186
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1187
- }
1188
- const [response, requestErr] = await client.request({
1189
- method: "POST",
1190
- path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
1191
- params: {
1192
- "rowNumber": 1,
1193
- [config.dpParam]: objectName
1194
- },
1195
- headers: {
1196
- "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
1197
- },
1198
- body: sqlQuery
997
+ const [dataFrame, error] = await previewData(client, {
998
+ objectName,
999
+ objectType,
1000
+ sqlQuery,
1001
+ limit: 1
1199
1002
  });
1200
- if (requestErr) {
1201
- return err(requestErr);
1202
- }
1203
- if (!response.ok) {
1204
- const text2 = await response.text();
1205
- const errorMsg = extractError(text2);
1206
- return err(new Error(`Row count query failed: ${errorMsg}`));
1003
+ if (error) {
1004
+ return err(new Error(`Row count query failed: ${error.message}`));
1207
1005
  }
1208
- const text = await response.text();
1209
- const [doc, parseErr] = safeParseXml(text);
1210
- if (parseErr) {
1211
- return err(parseErr);
1212
- }
1213
- const dataElements = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
1214
- if (dataElements.length === 0) {
1006
+ const countValue = dataFrame.rows[0]?.[0];
1007
+ if (countValue === void 0) {
1215
1008
  return err(new Error("No count value returned"));
1216
1009
  }
1217
- const countText = dataElements[0]?.textContent?.trim();
1218
- if (!countText) {
1219
- return err(new Error("Empty count value returned"));
1220
- }
1221
- const count = parseInt(countText, 10);
1010
+ const count = parseInt(String(countValue), 10);
1222
1011
  if (isNaN(count)) {
1223
1012
  return err(new Error("Invalid count value returned"));
1224
1013
  }