sprucebot-skills-kit-server
This module relies heavily on koajs and koa-router. It can help to be familiar with those modules, but it's probably not 100% necessary.
Where to start?
If you haven't already, you should totally checkout the sprucebot-skills-kit's documentation. In fact, this readme is assuming you already read it.
File structure
It is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.
.vscode
- Settings for your favorite IDE.controllers
- For built incontrollers
that are made available in every skill.auth.js
- An authentication endpoint. Also, a condition role set for whenDEV_MODE
is enabled in your skill.
factories
- Factories for helping us setup and run your skill.context.js
- Reusable factory for dropping things onto yourctx
. Used to populateservices
andutilities
.listeners.js
- Sets up all yourlisteners
, which are.js
files that exist inserver/events
in your skill.routes.js
- Sets up yourcontrollers
.wares.js
- Sets up your middleware.
helpers
- Simpleutilities
we make available to your skill.lang.js
- Handles language support. TODO: move to separate module and import.
middleware
- Built-in middleware that works on all skills.auth.js
- Handles authorization, i.e. locks routes by role.
node_modules
- Nodejs stuff.services
- Built-inservices
.uploads
- Built in upload adapters.s3.js
- For uploads to S3.
uploads.js
- Handles picking the upload adapter and passing it your file.
support
- Built-in configs and settings made available to your skill.errors.js
- Built-in errors.
utilities
- Built-inutilities
made available to your skill.auth.js
- Helpful methods for checking role hierarchy.
Checking permissions
Lets say you want to send an alert to the team when a user
arrives. But, you have rules around how it should work.
guest
arrives -> notifyteammates
andowners
teammate
arrives -> notifyowners
owner
arrives -> no notification
Using the built-in auth
utility
, you have the following.
auth.isAbove(teammate, guest)
auth.isAboveOrEqual(teammate, guest)
You should check the source of utilities/auth
in this module to see how it works.
For the rules defined above, we'll use auth.isAbove()
.
// server/events/did-enter.jsmoduleexports = async { // let sprucebot to it's usual (which is nothing on did-enter) try // we are probs gonna want special error reporting here so we can know the context // of the failure. remember, everything good goes in utilities or services await ctxservicesalerts catch err // a helpful message about the error to help us track it down from the logs console // followed be the actual error console }
Now we'll create our service
for sending the alerts.
// server/services/alerts.jsmoduleexports = // an event object mirrors a user object, so this works 100% async { // load all teammates const teammates = await thissb const sendTo = teammates //send to everyone await PromiseallsendTo }
For the sake of it, lets define our lang.
// lang/default.jsmoduleexports = `Hey , has arrived!`
Uploading files
Currently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.
We'll start on the interface
with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the user
to upload a file after they tap a fancy <Button />
.
We're going to depend on newer browser features, including FileReader
to make this work. Also, we'll only let them upload a pdf.
// interface/pages/owner/index.js { superprops thisstate = errorMessage: undefined } // setup the file reader when client side { // is browser out-to-date if typeof FileReader === 'undefined' this else // setup file reader, we're thisreader = thisreaderonload = thisonFileReaderLoadFile thisreaderonerror = thisonFileReaderLoadFileFail thispropsskill thispropsactionsfiles } // tiggered when clicking our nice <Button /> { // triggers the "select file" prompt thisfileInput } // triggered when a file is selected { // pull the first file (only one at a time for this example) const file = etargetfiles0 // always good to do a mime-type check if filetype !== 'application/pdf' this return // read the file using the reader thisreader } // called when the FileReader has read the whole file { const content = etargetresult const name = etargetname // defined in our actions in the code sample below thispropsactionsfiles } // if the FileReader fails for some reason { console this } { const lang files = thisprops const errorMessage = thisstate // errors can be set in our state or by an action failing const error = errorMessage || filesuploadError && filesuploadingErrorfriendlyMessage return <Container className="ownerDashboard"> !error && <BotText>lang</BotText> error && <BotText>error</BotText> filesfilevalue && <BotText>`Current file url: `</BotText> <Button busy=filesuploading primary onClick=thisselectFile> lang </Button> <input type="file" ref= { thisfileInput = input } onChange=thisonFileSelect style= display:'none' /> </Container> }
Some things to notice in the above example:
- We can manually set an error using
state
, but also errors inactions
are reported throughprops
. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver. - We use
<Button busy={files.uploading}>
to show a nice<Loader />
inside the button while the upload is in progress. - We check
files.file.value
for the currently uploaded file. This is actually theurl
of the file which is saved asmeta
after upload (which is why we checkvalue
)
Lets move into the action
for this upload process.
// interface/store/actions/files.jsconst FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR' const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR' { return types: FETCH_FILE_REQUEST FETCH_FILE_SUCCESS FETCH_FILE_ERROR client } { return types: UPLOAD_FILE_REQUEST UPLOAD_FILE_SUCCESS UPLOAD_FILE_ERROR client }
Don't forget to let your interface
know your new action exists.
// interface/store/actions/index.js moduleexports = users locations files
Ok, time for the reducer
.
// interface/store/reducers/files.js { }
Expose your reducer
to the interface
.
// interface/store/reducers/index.js moduleexports = users locations files
Ok, interface
is good to go. Lets setup the controller
on the server
to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in meta
for when we want it later. In this example, we're going to save the file for the location
.
// controllers/owner/files.jsmodule { router router}
Ok, we're almost there! We need to configure our uploads service
to work properly.
// config/default.jsmoduleexports = ... services: uploads: uploader: './uploads/s3.js' options: Bucket: 'my-bucket-name' accessKeyId: processenvAWS_ACCESS_KEY_ID secretAccessKey: processenvAWS_SECRET_ACCESS_KEY
That's it! Now, if you want to create your own upload service
, you could do this.
// config/default.jsmoduleexports = ... services: uploads: uploader: path options: endpoint: processenvFTP_ENDPOINT path: processenvFTP_PATH
Now, when you call ctx.services.files.upload()
it'll invoke your service
's upload()
method.
Note: Make sure you define init(options)
in your uploader. It'll receive whatever is defined in config/default.js
-> services.uploads.options
.
What's next?
Hmm, tbd on this one.