init
command to bootstrap new projects.cds init bookshop --add samples
cd bookshop
npm install
in-memory
option ensures that the database schema is deployed to the SQLite database, while the to
option controls the web protocol used to expose the data services. And the watch
option to restart the server automatically when file changes happen. For the last option, you need to add a new development dependency.npm add -D @sap/cds-dk
cds serve all --watch --in-memory --to rest
http://localhost:4004/catalog/Books
. I recommend doing this with the REST Client extension for Visual Studio Code and a new file called requests.http
:curl
to send this request as well.POST
endpoint in your web service. For this, you need to add the following line in the catalog definition of srv/cat-services.cds
.using my.bookshop as my from '../db/data-model';
service CatalogService {
@readonly
entity Books as projection on my.Books;
action submitOrder(book : Books:ID, quantity : Integer);
}
srv/cat-service.js
. As mentioned above, this action will parse the incoming request and use the parameters to reduce the value of the stock
field. If the value of that field turns negative, the request needs to fail:const cds = require("@sap/cds");
class CatalogService extends cds.ApplicationService {
init() {
const { Books } = cds.entities("my.bookshop");
// Reduce stock of ordered books if available stock suffices
this.on("submitOrder", async (req) => {
const { book, quantity } = req.data;
let { stock, title } = await SELECT`stock, title`.from(Books, book);
const remaining = stock - quantity;
if (remaining < 0) {
return req.reject(409, `${quantity} exceeds stock for book #${book}`);
}
await UPDATE(Books, book).with({ stock: remaining });
return { ID: book, stock: remaining };
});
return super.init();
}
}
module.exports = { CatalogService };
watch
option, the service restarts automatically once you save the file.requests.http
file to define the second request template:### Get stock data from the catalog
GET http://localhost:4004/catalog/Books
### Submit an order
POST http://localhost:4004/catalog/submitOrder HTTP/1.1
Content-Type: application/json
{
"book": 1,
"quantity": 95
}
curl
here to send the request via the terminal.curl -X POST -d '{"book":1,"quantity":95}' -H 'Content-Type: application/json' http://localhost:4004/catalog/submitOrder
npm add twilio
default-env.json
file is the perfect place for these secrets as it's already on the .gitignore
list, and all properties are automatically loaded in the environment variables during startup. For this application, replace all placeholder values such as the sender and receiver number of the text messages to the new file default-env.json
as well:{
"TWILIO_ACCOUNT_SID": "<Replace with Account SID>",
"TWILIO_AUTH_TOKEN": "<Replace with Auth Token>",
"TWILIO_SENDER": "<Replace with number in this format: +18600000000>",
"TWILIO_RECEIVER": "<Replace with number in this format: +18600000000>"
}
srv/cat-service.js
:const cds = require("@sap/cds");
const twilio = require("twilio");
const twilioClient = twilio();
class CatalogService extends cds.ApplicationService {
init() {
const { Books } = cds.entities("my.bookshop");
// Reduce stock of ordered books if available stock suffices
this.on("submitOrder", async (req) => {
const { book, quantity } = req.data;
let { stock, title } = await SELECT`stock, title`.from(Books, book);
const remaining = stock - quantity;
if (remaining < 0) {
return req.reject(409, `${quantity} exceeds stock for book #${book}`);
}
await UPDATE(Books, book).with({ stock: remaining });
if (remaining < 10) {
twilioClient.messages
.create({
body: `A customer just ordered ${quantity}x "${title}" and there are only ${remaining} left in stock.`,
from: process.env.TWILIO_SENDER,
to: process.env.TWILIO_RECEIVER,
})
.then((message) =>
console.log(`Message ${message.sid} has been delivered.`)
)
.catch((message) => console.error(message));
}
return { ID: book, stock: remaining };
});
return super.init();
}
}
module.exports = { CatalogService };
localhost
, you need to open a tunnel to route traffic to your machine from the Twilio data center. For this, you'll use ngrok.srv/cat-service.js
:twilioClient.messages
.create({
body: `A customer just ordered ${quantity}x "${title}" and there are `+
`only ${remaining} left in stock. Please respond with "Yes" `+
`if you would like to restock now.`,
from: process.env.TWILIO_SENDER,
to: process.env.TWILIO_RECEIVER,
})
srv/server.js
and listen to the bootstrap
event before you can initialize the client. Also include thewebhook()
middleware to prevent misuse by making sure only servers in Twilio data centers can call this webhook in production.const cds = require("@sap/cds");
var bodyParser = require("body-parser");
const twilio = require("twilio");
const MessagingResponse = twilio.twiml.MessagingResponse;
cds.on("bootstrap", (app) => {
const twilioClient = twilio();
app.use(bodyParser.urlencoded({ extended: true }));
app.post(
"/webhook",
twilio.webhook({ validate: process.env.NODE_ENV === "production" }), // Don't validate in test mode
async (req, res) => {
req.res.writeHead(200, { "Content-Type": "text/xml" });
res.end({ ok: 200 });
}
);
});
app.post(
"/webhook",
twilio.webhook({ validate: process.env.NODE_ENV === "production" }), // Don't validate in test mode
async (req, res) => {
req.res.writeHead(200, { "Content-Type": "text/xml" });
const twiml = new MessagingResponse();
if (req.body.Body.includes("Yes")) {
const parsed = await collectBookDetails(req.body.From, req.body.Body);
if (parsed?.book?.ID && parsed?.book?.stock) {
const newStock = parsed?.book.stock + parsed.restock;
await cds.update("Books").where({ ID: parsed?.book.ID }).with({
stock: newStock,
});
twiml.message(
`Great, your supplier has been contacted, and tomorrow there will be ${newStock} items in stock.`
);
} else {
twiml.message("Oh no, something went wrong. ");
}
} else {
twiml.message(
`I'm sorry, I don't understand that reply. Please answer with "Yes" or "Yes, order 60 additional book."`
);
}
res.end(twiml.toString());
}
);
collectBookDetails
function in srv/server.js
that reads contextual data from the last message sent to the bookshop manager. Add the new function as an inner function right after the declaration of the twilioClient
to make sure it is in scope. const twilioClient = twilio();
async function collectBookDetails(sender, message) {
const lastMessages = await twilioClient.messages.list({
limit: 1,
from: process.env.TWILIO_SENDER,
to: sender,
});
const lastMessage = lastMessages[0]?.body;
if (lastMessage) {
const restockPattern = /\d+/;
const lastOrderPattern = /(\d+)x/;
const titlePattern = /"(.*?)"/;
const restock = message.match(restockPattern)
? +message.match(restockPattern)[0]
: undefined;
try {
const lastOrder = +lastMessage.match(lastOrderPattern)[1];
const title = lastMessage.match(titlePattern)[1];
const books = await cds.read("Books").where({ title });
return {
restock: restock || lastOrder,
book: books[0],
};
} catch (err) {
//regex didn't find a last order or book title
return {};
}
}
}
app.use(bodyParser.urlencoded({ extended: true }));
requests.http
, replace the placeholder with your phone number, and trigger the request.%2b
### Test endpoint to restock books
POST http://localhost:4004/webhook HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Body=Yes 400&From=%2b18600000000
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Great, your supplier has been contacted, and tomorrow there will be 405 items in stock.</Message>
</Response>
ngrok http 4004
/webhook
suffix to the section “A message comes in”.You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
18 | |
10 | |
8 | |
6 | |
4 | |
4 | |
3 | |
3 | |
3 | |
2 |