# check if ssh is enabled
cf ssh-enabled myapp
# if it's not, enable it and restart app
cf enable-ssh myapp
cf restart myapp
# access with
cf ssh myapp
[JavaScript] Itereate through array of nested objects and delete specific objects
[
{
"NodeId": 1,
"HierarchyLevel": 1,
"type": "folder",
"nodes": [
{
"NodeId": 2,
"HierarchyLevel": 2,
"type": "folder",
"nodes": [
{
"NodeId": 3,
"HierarchyLevel": 3,
"type": "category"
}
]
},
{
"NodeId": 4,
"HierarchyLevel": 2,
"type": "category",
"nodes": [
{
"NodeId": 5,
"HierarchyLevel": 3,
"type": "file"
}
]
}
]
},
{
"NodeId": 6,
"HierarchyLevel": 1,
"type": "folder",
"nodes": [
{
"NodeId": 7,
"HierarchyLevel": 2,
"type": "category"
}
]
}
]
My task was to get rid of every Node which has no subnodes of type file at the last level of the hierachy. So for this example the result I needed was an array containing only the nodes 1,2,4,5.
Of course in reality the nested structure was way more complex. My approach was a recursive function which checks every element’s type and nodes length property and calls itself if there are any subnodes. Also it is recommended to loop backwards through the array while deleting from it.
const removeEmptyNodes = nodes => {
for (let i = nodes.length - 1; i > -1; i--) {
const n = nodes[i]
//call function recursive to go deeper through the nested structure
if (n.nodes) removeEmptyNodes(n.nodes)
//remove element if it's not a file and has no subnodes
if (n.type !== 'file' && (!n.nodes || n.nodes.length === 0)) nodes.splice(i, 1)
}
}
// nodes contains the array data from above
removeEmptyNodes(nodes)
[JavaScript] Copy an array of objects to a new array without any object references
The user tim-montague posted a great overview on the different deep copy techniques on stackoverflow:

In my case I had an array of objects which I wanted to copy without reference, therefore I used the option in the middle.
const copy = JSON.parse(JSON.stringify(myArray))
# Another way using ES6 Syntax
const copy = myArray.map(object => ({ ...object }))
[nodejs] Parsing multipart/mixed response (containing a file stream)
Recently I had to consume an API which returned multipart/mixed data. A response looked like this:
--Boundary_0000000000001
Content-Type: application/octet-stream
Content-Disposition: attachment; filename"test.pdf"
%PDF-1.7
%�������
1 0 obj
...
%%EOF
--Boundary_0000000000001
Content-Type: application/json
{"data":[]}
--Boundary_0000000000001--
There are some node packages for parsing multipart responses, but most can only handle multipart/formData and not multipart/mixed. The most recommended package for multipart/mixed is Dicer, but to be honest, I wasn’t sure how to use it properly. Therefore, I built my own parser. Luckily the user idbehold provided a function to parse a response string into a json object here. To get it working, I just had to change the regex expressions in the split function. The most important step is to convert the data from the arrayBuffer to a String in binary encoding before parsing.
Also, I wrote two helper functions. The first one to parse the boundary string from the Content-Type and the second one to parse the filename from the Content-Dispositon Header of your response.
module.exports = new class multipartMixedParser {
parse(boundary, buffer) {
const body = buffer.toString('binary') //toString encodes to utf-8 as default, this would lead to corrupted pdf's
return body.split(boundary).reduce((parts, part) => {
if (part && part !== '--\r\n') {
const [head, body] = part.trim().split(/\r\n\r\n/g)
console.log({ body })
parts.push({
body: body,
headers: head.split(/\r\n/g).reduce((headers, header) => {
const [key, value] = header.split(/:\s+/)
headers[key.toLowerCase()] = value
return headers
}, {})
})
}
return parts
}, [])
}
getBoundaryFromResponseHeaders(headers) {
//example: multipart/mixed;boundary=Boundary_0000000000001 -> --Boundary_0000000000001
const contentType = headers.get('content-type')
return '--' + contentType.split("=")[1].split(";")[0]
}
getFileNameFromContentDisposition(cd) {
//example: 'attachment; filename="example.pdf"' -> example.pdf
return cd.slice(
cd.indexOf('"') + 1,
cd.lastIndexOf('"')
)
}
}
And that’s how I’m calling the API and using the multipartMixedParser Class to parse the response. The API I was using is expecting a file as formData and is also returning a file (as part of the multipart/mixed response).
It’s important to get the buffer from the response. If you would use response.getText() it would convert the data to an utf-8 encoded string which will lead to corrupted files.
Please note, I’m using node-fetch. When using Axios, the response object will look different.
const btoa = require('btoa')
const FormData = require('form-data')
const fetch = require('node-fetch')
const multipartMixedParser = require('./multipartMixedParser')
function callAPI(file) {
const form = new FormData()
form.append('file', file.content, {
contentType: file.mediaType,
filename: file.fileName
})
const headers = {
'Authorization': 'Basic ' + btoa(username + ':' + password),
...form.getHeaders()
}
const url = /my/api/path
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: form
})
if (!response.ok) throw new Error(response.statusText)
//parse the response
const buffer = await response.buffer()
const boundary = multipartMixedParser.getBoundaryFromResponseHeaders(response.headers)
const result = multipartMixedParser.parse(boundary, buffer)
// in my case I only returned the file content as buffer and filename
return {
fileContent: Buffer.from(result[0].body, 'binary'),
fileName: multipartMixedParser.getFileNameFromContentDisposition(result[0].headers["content-disposition"])
}
} catch (err) {
console.log("Error message: " + err.message)
}
}
[JavaScript] Function to check if something is an array and if not, return it as array
const asArray = x => Array.isArray(x) ? x : [ x ]
[SAPUI5] Get i18n texts
To simply get access to i18n texts, I useally add this helper function to my BaseController.js
// helper for direct access to the ResourceBundle getText() function
getText : function (sTextKey, aParamter) {
return this.getOwnerComponent().getModel("i18n").getResourceBundle().getText(sTextKey, aParamter)
}
Texts can then be read in every controller with
// i18n: objects=Amount of objects: {0}
this.getText("objects", [iLength])
[JavaScript] Get the Start and End of the Day in format yyyy-MM-ddTHH:mm:ss.SSSZ
const start = new Date()
const end = new Date()
start.setHours(0, 0, 0, 0)
end.setHours(23, 59, 59, 999)
console.log(start.toISOString())
console.log(end.toISOString())
[CAP] Login to CF using a bash script
Create your script file, make it executeable and add it to your .gitignore as it contains sensitive information:
touch login.sh
chmod +x login.sh
echo login.sh >> .gitignore
Open the file and paste the following:
#! /bin/bash
cf login <<!
myemail@mail.com
mypassword
1
!
With “1” you select your target space. Save your script and run it using:
./login.sh
After some time, it can happen that the default identity provider of the SAP BTP (SAP ID service) is asking for a password change. I don’t know exactly, but it seems to be every 90 days?!
The login process will fail with the following output:
$ ./scripts/login.sh
API endpoint: https://api.cf.eu10.hana.ondemand.com
Email: myemail@mail.com
Password:
Authenticating...
{"error":"invalid_grant","error_description":"User authentication failed: PASSWORD_CHANGE_REQUIRED"}
To change your password, just go to https://account.sap.com or https://accounts.sap.com/, and it should directly open the password change screen.
Update 06.09.2024: The login can now also be done by completely using the cf command.
cf login -a https://api.cf.eu10.hana.ondemand.com -o myOrg -s mySpace -u myEmail@mail.com -p myPassword
[CAP] Create and deploy bookshop sample app
Since CDS 5.9.2 this is the quickest way of creating and deploying the bookshop sample:
cds init bookshop
cd bookshop
cds add samples
cds add hana
cds add xsuaa
cds add mta
npm install
mbt build
cf deploy mta_archives/bookshop_1.0.0.mtar
[Home Assistant] Open window reminder
This automation is triggered when a window stays open for >10 minutes. It will then send a reminder every 10 minutes (max 6 times).
This post helped me to create this script.
alias: Open window reminder
description: ''
trigger:
- platform: state
entity_id: binary_sensor.lumi_lumi_sensor_magnet_aq2_xxx_on_off
from: 'off'
to: 'on'
for:
hours: 0
minutes: 10
seconds: 0
condition: []
action:
- repeat:
while:
- condition: state
entity_id: binary_sensor.lumi_lumi_sensor_magnet_aq2_xxx_on_off
state: 'on'
- condition: template
value_template: '{{ repeat.index <= 6 }}'
sequence:
- variables:
counter: '{{ repeat.index * 10 }}'
- service: telegram_bot.send_message
data:
message: Window is open for {{ counter }} minutes
- delay: '00:10:00'
mode: single
