Security News
PyPI Introduces Digital Attestations to Strengthen Python Package Security
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
@sassoftware/restaf
Advanced tools
SAS® Viya® is like a Swiss Army knife when it comes to allowing all types of clients to talk to it—Java, Python, Lua, R, and, of course, SAS®. In addition to using these language-based APIs, SAS also publishes APIs that conform to REST best practices. The well-designed APIs in SAS Viya make it relatively easy to build complex web and non-web applications with SAS Viya as the engine of your application. The applications can be solely based on SAS Viya capabilities or users can do a mashup of the advanced SAS Viya analytics with their other products and capabilities. restAF is a light-weight, UI-framework agnostic, JavaScript library designed to be easy-to-use for app developers. restAF takes advantage of the consistency of the REST APIs to reduce the responses from the server into a standard form, and it also manages the data on the client side. This reduction enables developers to interact with the server in a repeating pattern regardless of which service the application is using.
Use restAF to exploit SAS® Cloud Analytic Services (CAS), compute server, SAS® Visual Analytics, and other key capabilities of SAS Viya in a very consistent manner with minimal coding.
The concepts and usage of restAF are explained through examples. Addtional examples can be found in these repositories.
restAF makes writing applications with SAS REST API simple
With restAF, you focus on your application and not on the nitty-gritty details of setting up HTTP calls, processing the returned results and HTTP codes, parsing the returned payload, managing the data, and so on.
restAF uses three key libraries(among others) that you are probably familiar with.
I am extremely grateful to the developers of these packages for their wonderful contributions.
In your web application you can access restaf with the following script tag.
<script src="https://unpkg.com/restaf/dist/restaf.min.js"></script>
A global variable called restaf is available for use in your javascript programs.
npm install restaf
and then your program
let restaf = require('restaf')
You can clone as follows
clone https://github.com/sassoftware/restaf
cd restaf
npm install
To build the code run this command
npm run build
restAF reduces the hypermedia returned from the REST API calls to a form that is suitable for rapid application development. THe next few sections will explain the reduction through examples and how you would use it to write your applications.
REST API response is an hypermedia. In practical terms a hypermedia has the following:
Links to
What you can do to with this resource
Where you can go next
Data
restAF stores all the response and returns an object referred to as rafObject*. The application will use this object to retrieve information from it use as it sees fit. The methods correspond to the various component parts of the response discussed above.
A link has several key parts:
The links are reduced to an object called rafLink. You will use rel to locate a rafLink in the rafObject. The primary use of rafLink is as a parameter to the api calls to the server.
Items are the data. Items can be a collection of items, a text string, an array, SVG, and so on. Some examples are as follows:
restAF manages the links and data for the application.
The flow of an application using restAF is as follows:
Here is a simple example running data step with compute service. In the example below ignore the following for now:
let restaf = require('restaf');
Preamble: Application specific code to get configuration information You might do it differently
let config = require('./config');
let logonPayload = config('raf.env');
Step 1: Initialize the store
let store = restaf.initStore();
Steps 2 and 3: Logon to the server and tell restAF which services you want to use
async function setup (store, logonPayload){
let msg = await store.logon(logonPayload);
let {compute} = await store.addServices('compute');
return compute;
}
// Step 4: The main program
async function mainLoop (store, compute, code) {
// get the list of contexts for compute server
// This of contexts as your SAS configs
let contexts = await store.apiCall(compute.links('contexts'));
// lookup the name of the context and create a SAS session with that information
// In the example we pick the first context that is returned.
// the itemList function returns the list of contexts
let context = contexts.itemsList(0);
let createSession = contexts.itemsCmd(context, 'createSession');
let session = await store.apiCall(createSession);
// Define the payload
// This is your SAS program
let payload = {
data: {code: code}
};
// Execute the data step and wait for completion
// All calls to the server are managed by the store you created in step 1
let job = await store.apiCall(session.links('execute'), payload);
let status = await store.jobState(job, null, 5, 2);
// Check the final status of job and view the logs
if (status.data === 'running') {
throw `ERROR: Job did not complete in allotted time`;
} else {
let logLines = await store.apiCall(status.job.links('log'));
// print the log
logViewer(logLines);
}
return 'restAF is cool or what';
}
Your program
let code = [`data _null_; do i = 1 to 100; x=1; end; run; `];
setup (store, logonPayload)
.then (compute => mainLoop(store, compute, code))
.catch(err =>console.log(err));
A Cool Fact - Notice that in the example above you see no reference to links, content-type, accept type, url's etc... Except in some rare cases you do not have to worry about these - restAF takes care of all those house keeping activities. You just focus on what your app needs to do.
restAF reduces the information returned by an API call an object called rafObject. This object has several properties amd methods. One key reduction is the transformation of links into rafLinks objects. rafLink allows restAF to navigate its own structures and make API calls on behalf of the application.
To initialize restAF you will make the following 2 calls:
###initStore
initStore must be called first to get the store object. Only one store per application . This call initializes the store to hold all the application data.
let restaf = require( 'restaf' );
store = restaf.initStore();
If using the umd version of the library, the global 'restaf' will be set when the umd version is brought in using the script tag.
At this point there is no data in the store and it is not attached to any Viya Server.
The next step is to associate Viya server with this instance of restAF.
store.logon( payload )
.then ( msg => console.log( msg ) )
.catch( err => console.log( err ) )
payload is either null or is an object with information to logon to the server. If null, restAF assumes that your browser session has been authenticated by other means. In an non-browser environment you have to pass in the valid payload.
Below is a typical payload for a nodejs application
payload = {
authType : 'password',
host : 'http://yourserver:portno',
user : username,
password : user password,
clientID : clientid, /* get this from your admin */
clientSecret: clientsecret /* get this from your admin */
} );
A friendly note to protect the payload information on your client machine since it has all the key authentication information in plain text.
In a browser use the initial logon payload will look as follows:
payload = {
authType: 'implicit',
host : 'http://yourserver:portno',
clientId: clientId, /* get this from your admin */
redirect: <The redirect that was specified when creating the clientId>
}
In the url that is the target of the redirect you will pass a null for payload
payload = null
restAF will parse the incoming location information and set up access to the server.
##Adding Services
After the initialization steps restAF still does not know what information to manage. Now populate restAF by registering the services you want to use in your application.
addServices method adds a folder for the each service. It additionally calls the root end point for that service and populates the folder with the links returned from that call. The addServices method returns a restAF object( rafObject) which we will discuss next.
All service names are case-sensitive
let {serviceName1, serviceName2,....} = await store.addServices( 'serviceName1', 'serviceName2', .... );
At this point serviceName1 is a rafObject with information for the root end points of that service.
let {compute, casManagement} = await store.addServices( 'compute', 'casManagement' );
##restAF Object(rafObject)
In your application you will be dealing primarily with rafObjects and rafLinks. Every time a result is returned from an API call(including addServices), restAF will "reduce" the information into a folder and return a rafObject
The folders are immutable objects based on immutable.js. while rafObject is a standard javaScript object.
The application will retrieve information using the methods of the rafObject.
restAF extends all the links returned by the server with information it needs to navigate the store and make the final calls. For an application the most common use of rafLinks is as an argument to apiCall method of the store to navigate to a new resource. The other common use is in associating links with UI components like buttons, navigators etc. This will be covered later in this document.
###apiCall - Making REST API calls
The apiCall method on the store object is the most used method for making REST calls.
store.apiCall( rafLink <, payload>)
rafLink is obtained using the links and itemsCmd methods on rafObject.
The payload(optional parameter based on the REST end point) is an object with the following optional keys
To run sas code with compute service
{ data: { code: \[ 'data a; x=1;run;' , 'proc print;run' \] }}
To run datastep in cas
{ action: 'datastep.runCode', data: { code: 'data a; x=1;run;' } }
To run upload action for CAS
let JSON_Parameters = {
casout: {
caslib: 'casuser', /* a valid caslib */
name : 'iris' /* name of output file on cas server */
},
importOptions: {
fileType: 'csv' /* type of the file being uploaded */
}
};
let payload = {
action : 'table.upload',
headers: { 'JSON-Parameters': JSON_Parameters },
data : readFile( 'iris', 'csv' )
}
To create a file with plain text
let payload = {
data : 'my data',
headers: { 'content-type': 'text/plain' }
}
apiCall method makes a single service call. The apiCallAll method on store allows the programmer to execute multiple service calls in parallel. A typical use case will be running forecast actions in parallel in different cas sessions. Another is running jobs in multiple compute server sessions. You can mix and match services in this single call( ex: create files, run cas jobs etc... but this would be an exception rather than a rule
store.apiCallAll( requestArray )
{ rafLink: <the rafLink to be called>
payload: <standard payload as described earlier in this document>
}
The method returns when all the calls in the requestArray return successfully or when one of them fails. In that sense apiCallAll behaves like PromiseAll. The order of the results array is the same as the requestArray. See the example for running parallel sas compute jobs
Some services might take a long time to run. Typically these services support a "state" link to query the state of the job. Note that not all services support the 'state' link. Services like compute service and JobExecution support this
In SAS API the state end point returns could be in one of these states.
running or pending - This means the job did not complete and is still running or pending.
Completed The job completed but the actual state of the job will vary based on the service. Some possible completed states are:
Most of the services that have a "state" end point recommend that you call the 'self' end point after the job completes to get the latest results from the job run. jobState and jobStateAll will make this additional call on 'self' if none of the jobs are still running. This saves you an additional call
At this point your code has to decide how best to proceed given these states.
store.jobState( rafObject-for-job <,payload> <,maxTries > )
Some services take a query parameter to support long polling - pass that information in the standard payload structure
maxTries is the number of times restAF will try if the job did not return a completed status
The returned object has the following form:
{
data : <completed|running|...|...>,
statusCode: <http code >,
job : <rafObject of the final job>
}
An example
/* pass a timeout of 2 seconds and try a max of 5 times */
let status = await store.jobState( job, {qs: { timeout: 2} }, 5 );
if (status.data === 'running') {
throw \`ERROR: Job did not complete in allotted time\`;
} else {
viewer(status.job); /* some viewer of results results */
switch(status.data) {
case 'warning': console.log(\`Warning: check your log for warnings\`); break;
case 'error':
throw \`Please correct errors and rerun program\`;
default:
break;
}
}
jobStateAll method to check the status of multiple jobs the jobs with a single call
The returned value has the form
{
running: <No of jobs still running >
details: >details on the completion ( see note above and see below for the form of this object>
jobState: [
{
job : < the job rafObject. see note below >
statusCode: < The http code from the server >
data : < text returned on completion( 'complete' for example ) >
}
]
}
Some notes
The details object gives you a summary of the returned state. It is easier to explain its structure with some examples
For jobState the detail will have a single key/value. The key will be the state returned from the call. For example if the job returned "failed" then the detail object will be as follows
detail: { failed: 1 }
For jobStateAll the detail could have multiple key/values. For example if you ran 4 jobs in parallel and 2 completed with state of 'warning' , 1 completed with state of 'completed' and one completed with state of 'error', then the detail will look as follows:
detail: {
warning : 2,
completed: 1,
error : 1
}
store.jobStateAll( array of jobs rafObject, payload, maxTries )
.then ( status => {
if ( status.running !== 0 ) {
...handle slow running jobs...
} else {
... check the detail object and make decisions...
}
} )
The payload parameter can be an array or single object. If single object then this payload will be applied to every rafObject in the first parameter. The primary purpose of the payload is to set timeout as a query parameter
Maxtries works the same as in jobState
REST APIs are great when the response to an api call is fast and instantaneous. In SAS it is not uncommon to have jobs that take a bit to run. For really long running jobs one should use our batch capabilities. However many solutions have applications where the user submits requests that can take from a few seconds to a minute or two to complete.
This presents a problem for the application writers.They have to poll the server at some intervals and probably at many points in their application code.
The submit method is designed to handle this scenario in a more elegant way.
The parameters to submit are:
store.submit( rafLink, payload, delay, id , <OnCompletion exit>, <progress exit> )
where:
raflink - same as for apiCall
payload - standard payload or null
delay - restAF will check the server after every "delay" seconds for completion and drive the callbacks if specified
id - specify an id(string). You will use this to retrieve the information the rafObject associated with this job
onCompletion - if non-null restAF will call this exit on completion ( success or failure) of the job.
progress - if non-null restAH will call this exist after every call to server to check the status.
The method signature of progress exit is
function progress ( data, id )
where
The function returns either true or false. If you return true, restAF will stop calling the server for the status of the job.
The method signature of onCompletion is
function onCompletion( err, jobStatus, id );
where
You will retrieve the final jobStatus using the submitStatus method of the store
let jobStatus = store.submitStatus(id);
where jobStatus is the same as what jobState returns.
One could write part of the introductory example as follows:
let payload = {
data: {code: [`data _null_; do i = 1 to 10000000; end;run; `]}
};
store.submit( session.links('execute'), payload, 1,
'myJob',onCompletion, progress);
###Properties
All property names are case sensitive
type: This indicates which of the results pattern this object represent
links: Use the links method below to view and obtain rafLinks for further calls. If type is "links" then this object only has links for other end points.
itemsList: The rafObject contains an array of items with id's.
itemArray: The returned value is an array of text. Use items methods to retrieve this data
items: The exact object type is not known. Use items method to get the information and also use resultType property do decide how to handle that data
resultType: The media type of the result
route: This is an index into the store for this specific result (see section on route later in this document). This will be useful if you want to pass a reference to a rafObject.
status: This the http code returned from the api call
statusInfo: An object with details of the status
host - the fully qualified Viay host the web app is logged on to.
Use these methods to interact with rafObject. These functions return either objects or string depending on the data. Remember these objects are immutable objects. In the document below the common usage patterns are described.
If the returned value is an rafObject you can use query parameters to drill into a specific value in that object. Examples are provided below
The links method will return an immutable object of rafLinks. You will use this method when the type of the rafObject is "links". But sometimes there will be links even when the type is not "links". In those cases the links are usually actions that operate on the whole collection
rafLink = rafObject.links( relName );
relName is the rel of the resource you are looking for. The call will return a rafLink that you can use in an apiCall as shown below
contentLink = fileObject.links( 'content' );
contentObject = store.apiCall ( contentLink );
Sometimes you need the entire list of links. An example is displaying buttons with the titles of the links. Below is an example of printing the titles of all the links.
allLinks = fileObject.links();
allLinks.forEach( ( l, rel ) => {
console.log(\`Title: ${rel} rel: ${rel} \` );
}
If the rafObject type is 'itemsList' use this method to get the immutable array of id's in the current result object. This is really a convenience method for UI ( ex: showing a list of the id's).
Below is an example listing all the id's
rafObject.itemsList().map( id => console.log(id) )
you can also do the following:
console.log( JSON.stringify(rafObject.itemsList(), null, 4) );
This method returns the rafLinks associated with pagination. This method can be used to get a rafLink associated with next, prev, last and first cmds(rel). At any given time the server may return some, all or none of these. To ensure safe programming, always check if the returned rafLink is non-null.
let nextCmd = rafObject.scrollCmds( 'next' );
if ( nextCmd !== null ) {
store.apiCall( nextCmd )
.then ( restafobj => {...do something...} )
} else {
console.log( 'No more data' )
}
In an UI you would want to display buttons for scrolling. A typical code will look like this
let cmds = rafObject.scrollCmds();
cmds.forEach( ( c, cmd ) => {
...make your scrolling menu...
} );
For example here is the code for creating a menubar in Reactjs
function ButtonMenu( props ) {
let { cmds, onClick } = props;
let menu = [];
cmds.forEach( ( c, rel ) => {
menu.push( <button key={rel} onClick={onClick(...)}
className="button"> {rel} </button> );
} );
return (
<div>
{menu}
</div>
)
}
This method gives you access to the data that might have been returned. This could be log for compute server, cas results, tables, status, ods and so on. The combination of resultType and type should give you sufficient information on how to process this data. It is possible to write an intelligent UI that determines the "viewer" to use given these pieces of information. But in a typical scenario the application knows what it is expecting at any specific point in the flow of the application.
The items method takes arguments that determines what is returned.
let result = rafObject.items( <query> )
Let us examine a few typical cases
All Items
let allItems = rafObject.items();
If you get all the items then you need to write the code to manage the items based on the resultType
Get a specific item using the id ( which you will get from itemsList() )
let item = rafObject.items( idOfItem );
**Getting data for an item in a collection **
let data = rafObject.item( idOfItem, 'data' );
Use this method to obtain the cmds associated with a particular item. Obtain the id of the item by using the itemList method. The most common usage is to get the rafLink for a specific command for the selected item
Get a specific command associated with a item with a specific id
let item = rafObject.itemsCmd( idOfItem, 'name of command' );
ex:
let deleteCmd = rafObject.itemsCmd( '1234ID', 'delete' );
store.apiCall( deleteCmd )
.then ( f => { do whatever) }
.catch( err => { do whatever) }
Get all commands associated with a item with a specific id
let rafLinks = rafObject.itemsCmd( idOfItem );
You can step thru this object as follows
rafObject.itemsCmd( idOfItem ).forEach( ( c, key) => {
// c is the current cmd information
// key is the name of the command( createSession, delete etc... )
}
On occasion one needs to access to the returned headers like etag. For those scenarios use the responseHeaders method.
Get a specific header
let etag = rafObject.responseHeaders( 'etag' );
Get all headers
let headers = rafObject.responseHeaders();
To get the http status returned from the call
let status = rafObject.status();
Use this to get any additional information that might have been returned for the status. Mostly useful if the job failed for some reason
let info = rafObject.statusInfo() ;
console.log( info );
####raw Returns the raw response from the server
####config The http payload sent to the server (useful for debugging)
In restAF you will access cas actions in the following manner.
Below is an example showing uploading of a csv to cas and then getting its info
The example below uses the nodejs file i/o. If you are running from browser you will use the File Object.
async function example () {
// setup
await store.logon(payload);
let {casManagement} = await store.addServices('casManagement');
let servers = await store.apiCall(casManagement.links('servers'));
let casserver = servers.itemsList(0);
let session = await store.apiCall(servers.itemsCmd(casserver, 'createSession'),
{data: {name: 'mysessionname'}});
/
// setup header for upload and the rest of the payload
let JSON_Parameters = {
casout: {
caslib: 'casuser', /* a valid caslib */
name : filename /* name of output file on cas server */
},
importOptions: {
fileType: fileType /* type of the file being uploaded */
}
};
let p = {
headers: {'JSON-Parameters': JSON_Parameters},
data : readFile(filename, fileType),/* read the csv file from somewhere */
action : 'upload'
};
let actionResult = await store.runAction(session.links, payload);
prtUtil.view(actionResult, 'Result of upload action');
let deleteAction = await store.apiCall(session.links('delete'));
return "All Done";
}
function readFile (filename, fileType) {
return fs.readFileSync(`./data/${filename}.${fileType}`);
}
Below is a sample program to print the rows in the CAS tables
let data = result.items('tables', <name of the table>);
let itemRows = data.get('rows');
let columns = [];
data.get('schema').map(s => {
columns.push(s.get('name'));
});
itemRows.map((r)=> {
let row = {};
r.map((value, j) => {
row[columns[j]] = value;
});
console.log(JSON.stringify(row, null, 4));
});
restAF handles all the pagination for you. Use the scrollCmds as described above. It assumes that you will use the scrollCmds API to retrieve more information.
For example to get the next set of items do the following
let next = rafObject.scrollCmds( 'next' );
if ( next === null ) {
/\* tell user there is no more data */
} else {
store.apiCall( next )
.then ( nextObj => {
do whatever using teh rafObject methods
} );
.catch ( err => {
handle error conditions
} )
}
Replace next with prev,last ans first as appropriate.
Here is an example of printing to console all the items from a collection
let store = restaf.initStore();
// Pagination
async function example (store, logonPayload, counter) {
await store.logon(logonPayload);
let {files} = await store.addServices('files');
let filesList = await store.apiCall(files.links('files'));
printList(filesList.itemsList());
let next;
// do this loop while the service returns the next link
while(((next = filesList.scrollCmds('next')) !== null) ) {
filesList = await store.apiCall(next);
printList(filesList.itemsList());
}
return 'All Done';
}
const printList = (itemsList) => console.log(JSON.stringify(itemsList, null, 4));
example(store, payload, 10)
.then (status => console.log(status))
.catch(err => console.log(err));
Each rafObject has a route property. This is a string that is key to the data pointed to by rafObject. restAF uses the route to find the data associated with this key. If for whatever reason you need to maintain a "pointer" to a rafObject or to pass it in as a query parameter you can use this.
Route is useful if you want to pass a specific rafObject to another part of your program through some routing mechanism( ex: react-router). Given a route you can get the associated rafObject with the rafObject method
let myObject = store.rafObject(route);
restAF relies on the Oauth2 authentication supported by SAS Viya.
There are a few use cases as described below:
store,logon( null)
.then ( msg => <do your stuff> )
.catch( err => <error handling> )
There are situations where a valid token might exist. In that case use the following payload to store.logon
store.logon( {
host: "your viya server',
token: "your token
});
For nodejs applications you will need to use the password flow authentication method
let payload = {
authType : 'password',
host : 'http://yourserver:portno',
user : username,
password : user password,
clientID : clientid, /* get this from your admin */
clientSecret: clientsecret /* get this from your admin */
} );
store.logon ( payload )
.then ( () => ...do whatever your app does ...)
.catch( err => ...do recovery ... )
For web applications it is recommended that you use implicit flow authentication.
let payload = {
host : <Viya server host (ex: http://my.example.com)
clientID : <clientid>
redirect : <your redirect>,
authType : 'implicit',
};
store.logon ( payload )
.then ( () => ...do whatever your app does ...)
.catch( err => ...do recovery ... )
In your redirect uri pass null for the logon
store.logon(null)
.then(...)
let c = store.connection();
The connection method return information on the current connection.
{
"type": "trusted",
"host": "http://your-viya-host",
"tokenType": "bearer",
"token": "... your Oauth token ..."
}
let services = store.getServices();
This returns the list of services including "services" specific to restAF(see below)
let services = store.getService('name of service');
This returns raf object for a service that was setup using addServices. \
Use this method to request restAF to store your data and to retrieve that data
await store.setAppData( id, data )
where
id - id of this data (string)
data - an object that you want restAF to store
To retrieve the data use the getAppData method
let mydata = store.getAppData( id );
To retieve a specific item pass
let mydata1 = store.getAppData( id, name);
Unlike the other store api methods getAppData returns a standard javaScript object and not an immutable object.
See example appdata.js in the examples directory.
Warning The store will be lost when you close the browser session. While technically it is possible to save and restore the store this version of restAF does not support persistence.
FAQs
Library for writing SAS Viya application using REST APIs
The npm package @sassoftware/restaf receives a total of 67 weekly downloads. As such, @sassoftware/restaf popularity was classified as not popular.
We found that @sassoftware/restaf demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.