Skip to content

Commit 75020fb

Browse files
authored
Merge pull request #37 from oslabs-beta/ms/valid-input-error
Ms/valid input error
2 parents ca609e8 + b669038 commit 75020fb

File tree

6 files changed

+308
-43
lines changed

6 files changed

+308
-43
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,10 @@ dist
103103
# TernJS port file
104104
.tern-port
105105

106+
# Local
107+
ksqljsTest.js
108+
package-lock.json
109+
local_ignore/
110+
.gitignore
106111

107112

ksqldb/customErrors.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ class ksqlDBError extends Error {
1010

1111
// Ensure the name of this error is the same as the class name
1212
this.name = this.constructor.name
13-
13+
1414
// capturing the stack trace keeps the reference to your error class
1515
Error.captureStackTrace(this, this.constructor);
16-
16+
1717
// you may also assign additional properties to your error
1818
//this.status = 404
19+
Object.keys(error).forEach(property => {this[property] = error[property]});
1920
}
2021
}
2122

@@ -46,10 +47,22 @@ class InappropriateStringParamError extends QueryBuilderError {
4647
}
4748
}
4849

50+
class invalidArgumentTypes extends Error {
51+
constructor(message) {
52+
super(message);
53+
54+
this.name = this.constructor.name;
55+
// necessary?
56+
Error.captureStackTrace(this, this.constructor);
57+
58+
}
59+
}
60+
4961
module.exports = {
5062
ksqlDBError,
5163
QueryBuilderError,
5264
EmptyQueryError,
5365
NumParamsError,
54-
InappropriateStringParamError
66+
InappropriateStringParamError,
67+
invalidArgumentTypes
5568
};

ksqldb/ksqldb.js

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const axios = require("axios");
22
const https = require('node:https');
33
const http2 = require("http2");
44
const { ksqlDBError } = require("./customErrors.js");
5+
const validateInputs = require('./validateInputs.js');
56
const queryBuilder = require('./queryBuilder.js');
67
const builder = new queryBuilder();
78

@@ -48,10 +49,14 @@ class ksqldb {
4849
* Example: [{object that contains the metadata}, [data], [data], ...}]
4950
*/
5051
pull = (query) => {
52+
validateInputs([query, 'string', 'query']);
53+
54+
const validatedQuery = builder.build(query);
55+
5156
return axios
5257
.post(this.ksqldbURL + "/query-stream",
5358
{
54-
sql: query,
59+
sql: validatedQuery,
5560
},
5661
{
5762
headers:
@@ -65,8 +70,7 @@ class ksqldb {
6570
})
6671
.then((res) => res.data)
6772
.catch((error) => {
68-
console.error(error);
69-
throw new ksqlDBError(error);
73+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
7074
});
7175
}
7276

@@ -85,6 +89,9 @@ class ksqldb {
8589
* result if successful.
8690
*/
8791
push(query, cb) {
92+
validateInputs([query, 'string', 'query', true], [cb, 'function', 'cb', true]);
93+
const validatedQuery = builder.build(query);
94+
8895
return new Promise((resolve, reject) => {
8996
let sentQueryId = false;
9097
const session = http2.connect(
@@ -109,7 +116,7 @@ class ksqldb {
109116
);
110117

111118
const reqBody = {
112-
sql: query,
119+
sql: validatedQuery,
113120
Accept: "application/json, application/vnd.ksqlapi.delimited.v1",
114121
};
115122

@@ -118,6 +125,9 @@ class ksqldb {
118125
req.setEncoding("utf8");
119126

120127
req.on("data", (chunk) => {
128+
// check for chunk containing errors
129+
if (JSON.parse(chunk)['@type']?.includes('error')) throw new ksqlDBError(JSON.parse(chunk));
130+
// continue if chunk indicates a healthy response
121131
if (!sentQueryId) {
122132
sentQueryId = true;
123133
cb(chunk);
@@ -145,6 +155,8 @@ class ksqldb {
145155
* if the termination was successful.
146156
*/
147157
terminate(queryId) {
158+
validateInputs([queryId, 'string', 'queryId']);
159+
148160
return axios.post(this.ksqldbURL + '/ksql',
149161
{
150162
ksql: `TERMINATE ${queryId};`
@@ -161,8 +173,7 @@ class ksqldb {
161173
})
162174
.then(res => res.data[0])
163175
.catch(error => {
164-
console.error(error);
165-
throw new ksqlDBError(error);
176+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
166177
});
167178
}
168179

@@ -177,9 +188,13 @@ class ksqldb {
177188
* @return {Promise} a promise that completes once the server response is received, and returns the requested data.
178189
*/
179190
ksql(query) {
191+
validateInputs([query, 'string', 'query']);
192+
193+
const validatedQuery = builder.build(query);
194+
180195
return axios.post(this.ksqldbURL + '/ksql',
181196
{
182-
ksql: query
197+
ksql: validatedQuery
183198
},
184199
{
185200
headers:
@@ -193,8 +208,7 @@ class ksqldb {
193208
})
194209
.then(res => res.data[0])
195210
.catch(error => {
196-
console.error(error);
197-
throw new ksqlDBError(error);
211+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
198212
});
199213
}
200214

@@ -214,9 +228,8 @@ class ksqldb {
214228
* @return {Promise} a promise that completes once the server response is received, and returns a response object.
215229
*/
216230
createStream(name, columnsType, topic, value_format = 'json', partitions = 1, key) {
217-
if (typeof name !== 'string' || typeof columnsType !== 'object' || typeof topic !== 'string' || typeof partitions !== 'number') {
218-
return console.log("invalid input(s)")
219-
}
231+
validateInputs([name, 'string', 'name', true], [columnsType, 'array', 'columnsType', true], [topic, 'string', 'topic'], [partitions, 'number', 'partitions']);
232+
220233
const columnsTypeString = columnsType.reduce((result, currentType) => result + ', ' + currentType);
221234
const query = `CREATE STREAM ${name} (${columnsTypeString}) WITH (kafka_topic='${topic}', value_format='${value_format}', partitions=${partitions});`;
222235

@@ -230,10 +243,8 @@ class ksqldb {
230243
{},
231244
httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null,
232245
})
233-
.then(res => res)
234246
.catch(error => {
235-
console.error(error);
236-
throw new ksqlDBError(error);
247+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
237248
});
238249
}
239250

@@ -248,6 +259,8 @@ class ksqldb {
248259
* @returns {Promise} - a promise that completes once the server response is received, and returns a query ID
249260
*/
250261
createStreamAs = (streamName, selectColumns, sourceStream, propertiesObj, conditions, partitionBy) => {
262+
validateInputs([streamName, 'string', 'streamName', true], [selectColumns, 'array', 'selectColumns', true], [sourceStream, 'string', 'sourceStream', true], [propertiesObj, 'object', 'propertiesObj'], [conditions, 'string', 'conditions'], [partitionBy, 'string', 'partitionBy']);
263+
251264
const propertiesArgs = [];
252265
const selectColStr = selectColumns.reduce((result, current) => result + ', ' + current);
253266
// begin with first consistent portion of query
@@ -286,7 +299,9 @@ class ksqldb {
286299
httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null,
287300
})
288301
.then(res => res.data[0].commandStatus.queryId)
289-
.catch(error => console.log(error));
302+
.catch(error => {
303+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
304+
});
290305
}
291306

292307
//---------------------Create tables-----------------
@@ -305,6 +320,8 @@ class ksqldb {
305320
* @return {Promise} a promise that completes once the server response is received, and returns a response object.
306321
*/
307322
createTable = (name, columnsType, topic, value_format = 'json', partitions) => {
323+
validateInputs([name, 'string', 'name', true], [columnsType, 'array', 'columnsType', true], [topic, 'string', 'topic', true], [value_format, 'string', 'value_format', true], [partitions, 'number', 'partitions']);
324+
308325
const columnsTypeString = columnsType.reduce((result, currentType) => result + ', ' + currentType);
309326
const query = `CREATE TABLE ${name} (${columnsTypeString}) WITH (kafka_topic='${topic}', value_format='${value_format}', partitions=${partitions});`
310327

@@ -323,8 +340,7 @@ class ksqldb {
323340
httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null,
324341
})
325342
.catch(error => {
326-
console.error(error);
327-
throw new ksqlDBError(error);
343+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
328344
});
329345
}
330346

@@ -344,6 +360,8 @@ class ksqldb {
344360
* @returns {Promise} a promise that completes once the server response is received, returning a response object
345361
*/
346362
createTableAs = (tableName, source, selectArray, propertiesObj, conditionsObj) => {
363+
validateInputs([tableName, 'string', 'tableName', true], [source, 'string', 'source', true], [selectArray, 'array', 'selectArray', true], [propertiesObj, 'object', 'propertiesObj'], [conditionsObj, 'object', 'conditionsObj']);
364+
347365
let selectColStr = selectArray.reduce((result, current) => result + ', ' + current);
348366

349367
// expect user to input properties object of format {topic: ... , value_format: ..., partitions: ...}
@@ -417,6 +435,8 @@ class ksqldb {
417435
*/
418436
//---------------------Insert Rows Into Existing Streams-----------------
419437
insertStream = (stream, rows) => {
438+
validateInputs([stream, 'string', 'stream', true], [rows, 'array', 'rows', true]);
439+
420440
return new Promise((resolve, reject) => {
421441
const msgOutput = [];
422442

@@ -452,6 +472,9 @@ class ksqldb {
452472
req.setEncoding("utf8");
453473

454474
req.on("data", (chunk) => {
475+
// check for chunk containing errors
476+
if (JSON.parse(chunk)['@type']?.includes('error')) throw new ksqlDBError(JSON.parse(chunk));
477+
// continue if chunk indicates a healthy response
455478
msgOutput.push(JSON.parse(chunk));
456479
});
457480

@@ -480,13 +503,8 @@ class ksqldb {
480503
* the end of the array that includes the time that the data was inserted into the ksqldb.
481504
*/
482505
pullFromTo = async (streamName, timezone = 'Greenwich', from = [undefined, '00', '00', '00'], to = ['2200-03-14', '00', '00', '00']) => {
483-
if (!streamName || typeof timezone !== 'string' || !from
484-
|| typeof from[0] !== 'string' || typeof from[1] !== 'string' || typeof from[2] !== 'string' || typeof from[3] !== 'string'
485-
|| typeof to[0] !== 'string' || typeof to[1] !== 'string' || typeof to[2] !== 'string' || typeof to[3] !== 'string'
486-
|| from[0].length !== 10 || to[0].length !== 10 || from[1].length !== 2 || to[1].length !== 2 || from[2].length !== 2 || to[2].length !== 2 || from[3].length !== 2 || to[3].length !== 2
487-
) {
488-
return new Error('invalid inputs');
489-
}
506+
validateInputs([streamName, 'string', 'streamName', true], [timezone, 'string', 'timezone', true], [from, 'array', 'from', true], [to, 'array', 'to', true]);
507+
490508
const userFrom = `${from[0]}T${from[1]}:${from[2]}:${from[3]}`;
491509
const userTo = `${to[0]}T${to[1]}:${to[2]}:${to[3]}`;
492510
const userFromUnix = new Date(userFrom).getTime();
@@ -518,11 +536,12 @@ class ksqldb {
518536
* message (string): Detailed message regarding the status of the execution statement.
519537
*/
520538
inspectQueryStatus(commandId) {
539+
validateInputs([commandId, 'string', 'commandId', true]);
540+
521541
return axios.get(this.ksqldbURL + `/status/${commandId}`)
522542
.then(response => response)
523543
.catch(error => {
524-
console.error(error);
525-
throw new ksqlDBError(error);
544+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
526545
});
527546
}
528547

@@ -539,8 +558,7 @@ class ksqldb {
539558
return axios.get(this.ksqldbURL + `/info`)
540559
.then(response => response)
541560
.catch(error => {
542-
console.error(error);
543-
throw new ksqlDBError(error);
561+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
544562
});
545563
}
546564

@@ -557,8 +575,7 @@ class ksqldb {
557575
return axios.get(this.ksqldbURL + `/healthcheck`)
558576
.then(response => response)
559577
.catch(error => {
560-
console.error(error);
561-
throw new ksqlDBError(error);
578+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
562579
});
563580
}
564581

@@ -576,8 +593,7 @@ class ksqldb {
576593
return axios.get(this.ksqldbURL + `/clusterStatus`)
577594
.then(response => response)
578595
.catch(error => {
579-
console.error(error);
580-
throw new ksqlDBError(error);
596+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
581597
});
582598
}
583599

@@ -592,6 +608,8 @@ class ksqldb {
592608
* @return {Promise} this method returns a promise that returns a response object.
593609
*/
594610
terminateCluster(topicsToDelete = []) {
611+
validateInputs([topicsToDelete, 'array', 'topicsToDelete', true]);
612+
595613
return axios.post(this.ksqldbURL + `/ksql/terminate`, {
596614
"deleteTopicList": topicsToDelete
597615
}, {
@@ -604,8 +622,7 @@ class ksqldb {
604622
})
605623
.then(response => response)
606624
.catch(error => {
607-
console.error(error);
608-
throw new ksqlDBError(error);
625+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
609626
});
610627
}
611628

@@ -623,14 +640,16 @@ class ksqldb {
623640
* "message": "One or more properties overrides set locally are prohibited by the KSQL server (use UNSET to reset their default value): [ksql.service.id]"
624641
* }
625642
*
643+
* @param {string} propertyName - the name of the property to validate
626644
* @return {Promise} this method returns a promise that resolves to a boolean true if the property is allowed to be changed.
627645
*/
628646
isValidProperty(propertyName) {
647+
validateInputs([propertyName, 'string', 'propertyName', true]);
648+
629649
return axios.get(this.ksqldbURL + `/is_valid_property/${propertyName}`)
630650
.then(response => response)
631651
.catch(error => {
632-
console.error(error);
633-
throw new ksqlDBError(error);
652+
throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error;
634653
});
635654
}
636655
};

ksqldb/queryBuilder.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ class queryBuilder {
55
}
66

77
build = (query, ...params) => {
8-
// consider building custom errors
8+
// check for empty query
99
if (this._checkEmptyQuery(query)) throw new EmptyQueryError();
10+
1011
let output = this._bind(query, ...params);
12+
1113
return output;
1214
}
1315
_bind = (query, ...params) => {
@@ -33,10 +35,10 @@ class queryBuilder {
3335
return param;
3436
case "object":
3537
if (Array.isArray(param)) {
36-
if (param[0].includes(";")) {
38+
if (param[0]?.includes(";")) {
3739
throw new InappropriateStringParamError("string params not wrapped in quotes should not include semi-colons");
3840
}
39-
return `${param[0].replaceAll("'", "''")}`
41+
return `${param[0]?.replaceAll("'", "''")}`
4042
}
4143
throw new QueryBuilderError("object should not be passed in as query argument");
4244
case "function":

0 commit comments

Comments
 (0)