Parameterized/Prepared Statements usage pg-promise - javascript

I'm using koa v2 with pg-promise. I try to do a simple SELECT 2 + 2; within a Parameterized/Prepared Statement to test my setup:
// http://127.0.0.1:3000/sql/2
router.get('/sql/:id', async (ctx) => {
await db.any({
name: 'addition',
text: 'SELECT 2 + 2;',
})
.then((data) => {
console.log('DATA:', data);
ctx.state = { title: data }; // => I want to return num 4 instead of [Object Object]
})
.catch((error) => {
console.log('ERROR:', error);
ctx.body = '::DATABASE CONNECTION ERROR::';
})
.finally(pgp.end);
await ctx.render('index');
});
Which is rendering [Object Object] in the templates and returning this to the console from pg-monitor:
17:30:54 connect(postgres#postgres)
17:30:54 name="addition", text="SELECT 2 + 2;"
17:30:54 disconnect(postgres#postgres)
DATA: [ anonymous { '?column?': 4 } ]
My problem:
I want to store result 4 in ctx.state. I don't know how can I access it within [ anonymous { '?column?': 4 } ]?
Thank You for your help!
Edit:
I found another recommended(1) ways(2) to dealing with named parameters in the official wiki.
// http://127.0.0.1:3000/sql/2
router.get('/sql/:id', async (ctx) => {
const obj = {
id: parseInt(ctx.params.id, 10),
};
await db.result('SELECT ${id} + ${id}', obj)
.then((data) => {
console.log('DATA:', data.rows[0]['?column?']);
ctx.state = { title: data.rows[0]['?column?'] }; // => 4
})
.catch((error) => {
console.log('ERROR:', error);
ctx.body = '::DATABASE CONNECTION ERROR::';
})
.finally(pgp.end);
await ctx.render('index');
});
I changed the any object to result, which returning the raw text. Than I access number 4 like a javascript object. Am I doing something wrong? Is there another way to access this value?
What is the recommended, more faster, safer way of usage?

Since you are requesting just one value, you should use method one:
const {value} = await db.one({
name: 'addition',
text: 'SELECT 2 + 2 as value',
}); // value = 4
And for such example you cannot use types PreparedStatement or ParameterizedQuery, because they format query on the server side, and PostgreSQL does not support syntax like $1 + $1.
The real question is - do you really need those types?

Related

How to call multiple APIs using loop

Hi I'm making SPA react program
I have some question
I want to know how can I use this JSON data HackNews
const [storyIds, setStoryIds] = useState([]);
const list = [];
useEffect(() => {
Top_API().then((res) => {
this.res = res.data.slice(0, 3);
this.res.forEach((ele) => {
axios
.get("https://hacker-news.firebaseio.com/v0/item/" + ele + ".json")
.then((res) => {
list.push({
id: res.data.id,
title: res.data.title,
url: res.data.url,
user: res.data.by,
score: res.data.score
});
setStoryIds(list);
});
});
});
}, []);
this is my code i want to print this api data
I print JSON data like this
{JSON.stringify(storyIds[0])}
This code works well. However, from the storyIds[1] arrangement, it is not visible on the screen. I checked that it was output from the console.
And when I go to a different page,
If I output the contents of my code array, an error occurs that the array cannot be found when you return to the page.
ex)
{JSON.stringify(storyIds[0].title)}
If you write like the code above, an error occurs that the array is not defined.
I've been trying to solve this situation for three days now without a proper solution.
The code that you print on the screen is as follows.
<div className="n1">
<a href={JSON.stringify(storyIds[0])}>
{JSON.stringify(storyIds[0])}
</a>
<br />
by: {JSON.stringify(storyIds[0])}
</div>
<div className="n2">{JSON.stringify(storyIds[1])}</div>
<div className="n3">{JSON.stringify(storyIds[2])}</div>
</div>
the data look's like
[{"id":30186326,"title":"Facebook loses users for the first time","url":"https://www.washingtonpost.com/technology/2022/02/02/facebook-earnings-meta/","user":"prostoalex","score":994},{"id":30186894,"title":"In second largest DeFi hack, Blockchain Bridge loses $320M Ether","url":"https://blockworks.co/in-second-largest-defi-hack-ever-blockchain-bridge-loses-320m-ether/","user":"CharlesW","score":400}]
how can I print this API response my screen?
You need to use async await with Promise.all for waiting for response from API
useEffect(() => {
Top_API().then(async (res) => {
this.res = res.data.slice(0, 5);
Promise.all(
this.res.map(async (r) => {
return await axios.get(
"https://hacker-news.firebaseio.com/v0/item/" + r + ".json"
);
})
).then((resolved) => {
resolved.map((resolve) => {
list.push({
id: resolve.data.id,
title: resolve.data.title,
url: resolve.data.url,
user: resolve.data.by,
score: resolve.data.score
});
});
setStoryIds(list);
});
});
}, []);

Working SQL yields Syntax Error in pg-promise

I have the following code in one of my endpoints:
let setText = ''
for (const [ key, value ] of Object.entries(req.body.info)) {
setText = setText.concat(`${key} = ${value}, `)
}
// Last character always an extra comma and whitespace
setText = setText.substring(0, setText.length - 2)
db.one('UPDATE students SET ${setText} WHERE id = ${id} RETURNING *', { setText, id: req.body.id })
.then(data => {
res.json({ data })
})
.catch(err => {
res.status(400).json({'error': err.message})
})
It is supposed to dynamically generate the SQL from the request body. When I log the created SQL, it generates correctly. It even works when I directly query the database. However, whenever I run the endpoint, I get a syntax error that's "at or near" whatever setText is. I've tried using slice instead of substring with no change.
You should never concatenate values manually, as they are not escaped properly then, and open your code to possible SQL injections.
Use the tools that the library offers. For an UPDATE from a dynamic object, see below:
const cs = new pgp.helpers.ColumnSet(req.body.info, {table: 'students'});
const query = pgp.helpers.update(req.body.info, cs) +
pgp.as.format(' WHERE id = ${id} RETURNING *', req.body);
db.one(query)
.then(data => {
res.json({ data });
})
.catch(err => {
res.status(400).json({error: err.message})
});

Have I mismanaged a promise? or mismanaged a data type?

I would love to hear some ideas to solve this problem. Here's a javascript function. Make note of the comments, I'll refer to those specific lines later.
async function getGroupId(name) {
const response = await fetch(
environmentData.localFunctionsUrl + functionPath,
{
body: JSON.stringify({
command: `GetInterests`,
path: `/interest-categories/`
}),
method: `POST`
}
)
const groupId = await response.json().then((json) => {
return json.map((obj) => {
if(obj.name.substring(0, `YYYY-MM-DD`.length) === name.substring(0, `YYYY-MM-DD`.length)) {
return obj.id //LET'S CALL THIS LINE "yellow"
}
})[0] //AND THIS LINE, LET'S CALL IT "yellow-ish"
})
const retObj = { [groupId]: true } //LET'S CALL THIS LINE "orange"
return retObj
}
That function is called in my code like this:
async function registerSubscriber(data) {
const emailToHash = data.paymentIntent.metadata.contact_email
const response = await fetch(
environmentData.localFunctionsUrl + functionPath,
{
body: JSON.stringify({
command: `UpsertMember`,
path: `/members/`+ crypto.createHash(`md5`).update(emailToHash.toLowerCase()).digest(`hex`),
mailchimp_body: {
email_address: emailToHash,
merge_fields: {
COURSEDATE: data.paymentIntent.metadata.courseData_attr_name.substring(
0,
data.paymentIntent.metadata.courseData_attr_name.indexOf(`:`)
),
FNAME: data.paymentIntent.metadata.contact_firstname,
LNAME: data.paymentIntent.metadata.contact_surname
},
// HERE: NEXT LINE
interests: await getGroupId(data.paymentIntent.metadata.courseData_attr_name)
}
}),
method: `PATCH`
}
)
const json = await response.json()
return json
}
So, THIS ALL WORKS in my local environment. This code properly interacts with a lambda function I've created to interact with MailChimp's API.
Here's the problem: in my production environment (Netlify, fyi), line "yellow" is reached (a match is made in the if statement), so presumably there's an element available in line "yellow-ish".
But certainly groupId in line "orange" is undefined.
Theory 1:
My working theory is it's failing due to race-condition. I mean, perhaps line "orange" is being returned before line "yellow" produces the relevant data. But I'm (almost) certain I've structured the promises correctly -- the async and await keywords
Theory 2:
My other working theory is that I've mismanaged the data types. Is there a reason that {[groupId]:true} may work in a windows environment but not in a linux env (both env's running the same version of node).
Related to this theory: the value returned in line "yellow" sometimes begins with a number, sometimes with a letter. This shouldn't matter, but I mention it because line "orange" will sometimes like like this:
{ '123abc': true }
And sometimes without quotes like this:
{ abc123: true }
I presume this difference in syntax is a known behaviour in Javascript -- is it just how object keys are handled?
Something that caught my attention was the map inside the following method:
const groupId = await response.json().then((json) => {
return json.map((obj) => {
if(obj.name.substring(0, `YYYY-MM-DD`.length) === name.substring(0, `YYYY-MM-DD`.length)) {
return obj.id //LET'S CALL THIS LINE "yellow"
}
})[0] //AND THIS LINE, LET'S CALL IT "yellow-ish"
})
While this could provide you with the value you're looking for, it's generally bad practice, because a map would loop through the whole array and it'll replace each value of the array that doesn't match your conditions with undefined, so if the value you're looking for is not at index 0 then you'll most certainly get undefined when you run the method. The method you need is find which will get a value/object out of the array and make it directly accessible.
Try the following code:
async function getGroupId(name) {
const response = await fetch(
environmentData.localFunctionsUrl + functionPath,
{
body: JSON.stringify({
command: `GetInterests`,
path: `/interest-categories/`
}),
method: `POST`
}
);
const responseJson = await response.json();
const group = responseJson.find(obj => obj.name.substring(0, `YYYY-MM-DD`.length) === name.substring(0, `YYYY-MM-DD`.length));
return {
[group.id]: true
};
}
async function registerSubscriber(data) {
const {
paymentIntent: {
metadata: {
contact_email,
courseData_attr_name,
contact_firstname,
contact_surname
}
}
} = data;
const interests = await getGroupId(courseData_attr_name);
const response = await fetch(
environmentData.localFunctionsUrl + functionPath,
{
body: JSON.stringify({
command: 'UpsertMember',
path: `/members/${crypto.createHash(`md5`).update(contact_email.toLowerCase()).digest(`hex`)}`,
mailchimp_body: {
email_address: contact_email,
merge_fields: {
COURSEDATE: courseData_attr_name.substring(0, courseData_attr_name.indexOf(`:`)),
FNAME: contact_firstname,
LNAME: contact_surname
},
interests
}
}),
method: 'PATCH'
}
);
const json = await response.json();
return json
}
Unfortunately, I can't say for certain if your handling of the JSON response is wrong without any sample (dummy) data, but usually a JSON object is not an array, so you can't handle it with a map.
Side note, you should try to stick to either async/await or then/catch.
Your should work well, If you get groupId undefined then json.map((obj) => {...} returns empty array.
Try to debug or add console.log:
const groupId = await response.json().then((json) => {
console.log('server response', json);
return json.map((obj) => {
if(obj.name.substring(0, `YYYY-MM-DD`.length) === name.substring(0, `YYYY-MM-DD`.length)) {
return obj.id //LET'S CALL THIS LINE "yellow"
}
})[0] //AND THIS LINE, LET'S CALL IT "yellow-ish"
})
console.log('groupId', groupId);
const retObj = { [groupId]: true } //LET'S CALL THIS LINE "orange"
return retObj
PS: As mentioned in other comments you should stick to await or then and not try to mix them

How to trouble shoot "NaN" error when the call to a function and assigning to let is the only visible issue

I'm testing an Alexa skill locally and getting an error that just says NaN. I have figured out that line let recipe = getRecipe() is the problem through console.log() statements. It doesn't appear to be in the the getRecipe() function itself because a console.log() statement at the very beginning of the try block in that function does not run, but the one at the beginning of the catch does. Thanks in advance for any suggestions.
Handler:
handle(handlerInput){
const attributes = handlerInput.attributesManager.getSessionAttributes();
const request = handlerInput.requestEnvelope.request;
switch (attributes.previousIntent){
case "FoodIntent":
if(request.intent.slots.answer.resolutions.resolutionsPerAuthority[0].values[0].value.name === 'yes'){
let randomFood = Helpers.suggestFood(handlerInput);
let queryFood = randomFood.replace(/\s+/g, '-').toLowerCase(); event
attributes.currentSuggestedFood = queryFood;
const speechText = 'Great! In the future I will be able to look up the ingredients for you.'
console.log('before call getRecipe()')
let recipe = getRecipe(handlerInput)
console.log('After call getRecipe()')
return handlerInput.responseBuilder
.speak(speechText + " "+ recipe)
.reprompt(speechText)
.withShouldEndSession(true)
.withSimpleCard('Cheer Up - YesNo', speechText)
.getResponse();
} else {
let randomFood = Helpers.suggestFood(handlerInput);
let speechText = ResponseToUsersNo[Math.floor(Math.random() * ResponseToUsersNo.length)]+
FoodPrefixes[Math.floor(Math.random() * FoodPrefixes.length)] +
randomFood + FoodSuffixes[Math.floor(Math.random() * FoodSuffixes.length)];
let repromptText = 'Did the last suggestion work for you?'
handlerInput.attributesManager.setSessionAttributes(attributes);
if (attributes.FoodsAlreadySuggested.length >= 10) {
speechText = 'I feel like you don\'t actually want anything. So I\'m leaving for now, talk to you later.'
return handlerInput.responseBuilder
.speak(speechText)
.withShouldEndSession(true)
.withSimpleCard('Cheer Up - YesNo', speechText)
.getResponse();
}
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(repromptText)
.withSimpleCard('Cheer Up - YesNo', speechText)
.getResponse();
}
case "HobbyIntent":
if(request.intent.slots
And the getRecipe() function:
async function getRecipe(handlerInput) {
try{
console.log('before attributes')
const attributes = handlerInput.attributesManager.getSessionAttributes();
console.log('attributes: '+ attributes)
console.log('before url')
const url = `https://api.edamam.com/search?q=${attributes.currentSuggestedFood}&app_id=${FOOD_APP_ID}&app_key=${FOOD_APP_KEY}`; //&from=0&to=3&calories=591-722&health=alcohol-free this was on the end of the uri
console.log(url)
console.log('after url')
request.get(url, (error, response, body) => {
// let json = JSON.parse(body);
console.log('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body); // Print the body
//const theRecipe = await body;
const payload = JSON.parse(body)
console.log("The ingredients for "+ payload.q + " is: ")
console.log(payload.hits[0].recipe.ingredientLines)
return (payload.hits[0].recipe.ingredientLines);
});
}
catch(err){
console.log('before error statement in catch')
console.error('There was an error: ', + err)
}
};
Here is my output:
before call getRecipe()
before attributes
attributes: [object Object]
before url
https://api.edamam.com/search?q=rellenos-de-papa&app_id=b4dbea92&app_key=8d916c99b930b77c8cbb4615f0800df7
after url
before error statement in catch
There was an error: NaN
After call getRecipe()
{ version: '1.0',
response:
{ outputSpeech:
{ type: 'SSML',
ssml: '<speak>Great! In the future I will be able to look up the ingredients for you. The ingredients are [object Promise]</speak>' },
reprompt: { outputSpeech: [Object] },
shouldEndSession: true,
card:
{ type: 'Simple',
title: 'Cheer Up - YesNo',
content: 'Great! In the future I will be able to look up the
ingredients for you.' } },
userAgent: 'ask-node/2.3.0 Node/v8.12.0',
sessionAttributes:
{ foodType: 'PuertoRican',
FoodsAlreadySuggested: [ 'Platanos Maduros', 'Rellenos de Papa' ],
previousIntent: 'FoodIntent',
state: '_YES_NO',
currentSuggestedFood: 'rellenos-de-papa' } }
UPDATE:
#Shilly. So I'm still confused... An aside, I had to edit your function a bit to make the code inside the catch reachable... but anyway I think what I did still retains the core logic you were trying to impart.
My problem is that I get an error when I parse that says unexpected token o in JSON at position 1. I think this usually means I don't need to parse it because it's already a valid js object. Cool. So I remove the parse, but then I get Cannot read property '0' of undefined., referring of course to my return payload.hits[0].recipe.ingredientLines. Can't seem to wrap my head around why. Thanks a bunch for your help.
function getRecipe(handlerInput) {
const attributes = handlerInput.attributesManager.getSessionAttributes();
const url = `https://api.edamam.com/search?q=${attributes.currentSuggestedFood}&app_id=${FOOD_APP_ID}&app_key=${FOOD_APP_KEY}`; //&from=0&to=3&calories=591-722&health=alcohol-free this was on the end of the uri
return get( url, ( response, body ) => {
const payload = JSON.parse(body)
console.log(payload)
return payload.hits[0].recipe.ingredientLines;
}).catch( error => {
console.error( `failed GET request for: ${ url }` );
console.error( error );
});
};
Also here is the beginning of the body in the response, which doesn't look parsed to me... body: '{\n "q" : "tostones",\n "from" : 0,\n "to" : 10,\n "params" : {\n
Finally figured it out. Many thanks to #Shilly for guiding me in the proper direction. My understanding of async and await was wrong. These sources were helpful:
Returning handler.ResponseBuilder from promise.then() method
https://medium.com/#tkssharma/writing-neat-asynchronous-node-js-code-with-promises-async-await-fa8d8b0bcd7c
Here is my updated code:
The async handler relies on a function that I created to use Promises with #Shilly's help. It's probably not the most concise way, but it works!
Handler:
async handle(handlerInput){
const attributes = handlerInput.attributesManager.getSessionAttributes();
const request = handlerInput.requestEnvelope.request;
switch (attributes.previousIntent){
case "FoodIntent":
if(request.intent.slots.answer.resolutions.resolutionsPerAuthority[0].values[0].value.name === 'yes'){
let randomFood = Helpers.suggestFood(handlerInput);
let queryFood = randomFood.replace(/\s+/g, '-').toLowerCase();
attributes.currentSuggestedFood = queryFood;
const speechText = 'Great! Here are the ingredients!'
let recipe = await getRecipe(handlerInput)
let recipeIngredients = recipe.hits[0].recipe.ingredientLines;
return handlerInput.responseBuilder
.speak(speechText+ 'The ingredients are '+ recipeIngredients)
.reprompt(speechText)
.withShouldEndSession(true)
.withSimpleCard('Cheer Up - YesIntentFood', recipeIngredients)
.getResponse();
function:
async function getRecipe(handlerInput) {
const attributes = handlerInput.attributesManager.getSessionAttributes();
const url = `https://api.edamam.com/search?q=${attributes.currentSuggestedFood}&app_id=${FOOD_APP_ID}&app_key=${FOOD_APP_KEY}`;
console.log(url)
return new Promise (function(resolve, reject) {
request.get(url, (error, response, body) => {
if (error) {
reject(error);
} else {
resolve(JSON.parse(body))
}
});
})
};
Output:
https://api.edamam.com/search?q=pernil&app_id=b4dbea92&app_key=8d916c99b930b77c8cbb4615f0800df7
{ version: '1.0',
response:
{ outputSpeech:
{ type: 'SSML',
ssml: '<speak>Great! In the future I will be able to look up the ingredients for you.The ingredients are 2 1/2 pounds pork shoulder, boston butt, pernil,2 garlic cloves,1 small onion,1 bunch cilantro,1 jalapeƱo,1 cup orange juice,1 cup pineapple juice,1 lemon,Handfuls salt,Pepper to taste,Ground cumin</speak>' },
reprompt: { outputSpeech: [Object] },
shouldEndSession: true,
card:
{ type: 'Simple',
title: 'Cheer Up - YesIntentFood',
content: [Array] } },
userAgent: 'ask-node/2.3.0 Node/v8.12.0',
sessionAttributes:
{ foodType: 'PuertoRican',
FoodsAlreadySuggested: [ 'Platanos Maduros', 'Pernil' ],
previousIntent: 'FoodIntent',
state: '_YES_NO',
currentSuggestedFood: 'pernil' } }
const attributes = handlerInput.attributesManager.getSessionAttributes() throws an error NaN for reasons still unknown.
The catch() handler catches this error and logs it.
But since the function is async, and you don't resolve the promise inside the catch clause, you get this [object Promise] stringified version of that promise instead of the actual ingredient list.
Edit:
Have you considered using util.promisify() so you don't have to mix callbacks and promises?
const { promisify } = require( 'util' );
const request = require( 'request' );
const get = promisify( request.get );
function getRecipe(handlerInput) {
const attributes = handlerInput.attributesManager.getSessionAttributes();
const url = `https://api.edamam.com/search?q=${attributes.currentSuggestedFood}&app_id=${FOOD_APP_ID}&app_key=${FOOD_APP_KEY}`; //&from=0&to=3&calories=591-722&health=alcohol-free this was on the end of the uri
return get( url, ( response, body ) => {
const payload = JSON.parse(body)
return payload.hits[0].recipe.ingredientLines;
}).catch( error ) {
console.error( `failed GET request for: ${ url }` );
console.error( error );
});
};
Same can be written with async/await style, but I'm not fluent enough in it to get it 100% correct without being able to test the code myself.

How to get multiple DOM elements with chrome-remote-interface node js?

I am just trying to build a crawler with chrome-remote-interface but i don't know how to get multiple dom elements like specific targets id,classes.
for Ex:
price = document.getelementbyid('price')
name= document.getelementbyid('name')
Code
const CDP = require('chrome-remote-interface');
CDP((client) => {
// Extract used DevTools domains.
const {Page, Runtime} = client;
// Enable events on domains we are interested in.
Promise.all([
Page.enable()
]).then(() => {
return Page.navigate({url: 'http://example.com'})
});
// Evaluate outerHTML after page has loaded.
Page.loadEventFired(() => {
Runtime.evaluate({expression: 'document.body.outerHTML'}).then((result) => {
//How to get Multiple Dom elements
console.log(result.result.value);
client.close();
});
});
}).on('error', (err) => {
console.error('Cannot connect to browser:', err);
});
Update
const CDP = require('chrome-remote-interface');
CDP((client) => {
// Extract used DevTools domains.
const {DOM,Page, Runtime} = client;
// Enable events on domains we are interested in.
Promise.all([
Page.enable()
]).then(() => {
return Page.navigate({url: 'https://someDomain.com'});
})
Page.loadEventFired(() => {
const expression = `({
test: document.getElementsByClassName('rows')),
})`
Runtime.evaluate({expression,returnByValue: true}).then((result) => {
console.log(result.result) // Error
client.close()
})
})
}).on('error', (err) => {
console.error('Cannot connect to browser:', err);
});
Error
{ type: 'object',
subtype: 'error',
className: 'SyntaxError',
description: 'SyntaxError: Unexpected token )',
objectId: '{"injectedScriptId":14,"id":1}' }
Actually I want to iterate over the list of elements But I don't know where it goes wrong
You cannot move DOM object from the browser context to the Node.js context, all you can do is pass a property or whatever can be considered a JSON object. Here I'm assuming you're interested in the computed HTML.
A possible solution is:
const CDP = require('chrome-remote-interface');
CDP((client) => {
// Extract used DevTools domains.
const {Page, Runtime} = client;
// Enable events on domains we are interested in.
Promise.all([
Page.enable()
]).then(() => {
return Page.navigate({url: 'http://example.com'});
});
// Evaluate outerHTML after page has loaded.
Page.loadEventFired(() => {
const expression = `({
name: document.getElementById('name').outerHTML,
price: document.getElementById('price').outerHTML
})`;
Runtime.evaluate({
expression,
returnByValue: true
}).then(({result}) => {
const {name, price} = result.value;
console.log(`name: ${name}`);
console.log(`price: ${price}`);
client.close();
});
});
}).on('error', (err) => {
console.error('Cannot connect to browser:', err);
});
The key point is returning a JSON object using returnByValue: true.
Update: You have an error in your expression, a trailing ) in ...('rows')),. But even if you fix it you'd still end up in a wrong situation because you're attempting to pass an array of DOM objects (see the first paragraph of this answer). Again, if you want just the outer HTML you can do something like:
// Evaluate outerHTML after page has loaded.
Page.loadEventFired(() => {
const expression = `
// fetch an array-like of DOM elements
var elements = document.getElementsByTagName('p');
// create and return an array containing
// just a property (in this case `outerHTML`)
Array.prototype.map.call(elements, x => x.outerHTML);
`;
Runtime.evaluate({
expression,
returnByValue: true
}).then(({result}) => {
// this is the returned array
const elements = result.value;
elements.forEach((html) => {
console.log(`- ${html}`);
});
client.close();
});
});

Categories

Resources