pryv

Pryv Javascript Library

Usage no npm install needed!

<script type="module">
  import pryv from 'https://cdn.skypack.dev/pryv';
</script>

README

JavaScript library for Pryv.io

This JavaScript library is meant to facilitate writing NodeJS and browser apps for a Pryv.io platform, it follows the Pryv.io App Guidelines.

Contribute

Prerequisites: Node 12

  • Setup: npm run setup

  • Build pryv.js library for browsers: npm run build, the result is published in ./dist

  • Build documentation: npm run doc, the result is published in ./dist/docs

    Note: as per v2.1.7 jsdoc dev dependency has been removed from package.json .. it should be installed with npm install jsoc --dev

  • Node Tests: npm run test

  • Coverage: npm run cover, the result is visible in ./coverage

  • Browser tests: build, then npm run webserver and open https://l.rec.la:9443/tests/browser-tests.html?pryvServiceInfoUrl=https://zouzou.com/service/info

  • Update on CDN: After running setup and build scripts, run npm run gh-pages ${COMMIT_MESSAGE}. If this fails, run npm run clear to rebuild a fresh dist/ folder

Usage

Table of Contents

Import

Browser

<script src="https://api.pryv.com/lib-js/pryv.js"></script>

Others distributions for browsers & extensions:

Example on code pen:

Node.js

Install with: npm install pryv --save

const Pryv = require('pryv');

Obtaining a Pryv.Connection

A connection is an authenticated link to a Pryv.io account.

Using an API endpoint

The format of the API endpoint can be found in your platform's service information under the api property. The most frequent one has the following format: https://{token}@{api-endpoint}

const apiEndpoint = 'https://ck6bwmcar00041ep87c8ujf90@drtom.pryv.me';
const connection = new Pryv.Connection(apiEndpoint);

Using a Username & Token (knowing the service information URL)

const service = new Pryv.Service('https://reg.pryv.me/service/info');
const apiEndpoint = await service.apiEndpointFor(username, token);
const connection = new Pryv.Connection(apiEndpoint);

Within a WebPage with a login button

The following code is an implementation of the Pryv.io Authentication process.

<!doctype html>
<html>
<head>
  <title>Pryv - Javascript Lib</title>
  <script src="https://api.pryv.com/lib-js/pryv.js"></script>
</head>
<body>
  <span id="pryv-button"></span>
  <script>
    var connection = null;

    var authSettings = {
      spanButtonID: 'pryv-button', // span id the DOM that will be replaced by the Service specific button
      onStateChange: pryvAuthStateChange, // event Listener for Authentication steps
      authRequest: { // See: https://api.pryv.com/reference/#auth-request
        requestingAppId: 'lib-js-test',
        languageCode: 'fr', // optional (default english)
        requestedPermissions: [
          {
            streamId: 'test',
            defaultName: 'test',
            level: 'manage'
          }
        ],
        clientData: {
          'app-web-auth:description': {
            'type': 'note/txt', 'content': 'This is a consent message.'
          }
        },
        // referer: 'my test with lib-js', // optional string to track registration source
      }
    };

    function pryvAuthStateChange(state) { // called each time the authentication state changed
      console.log('##pryvAuthStateChange', state);
      if (state.id === Pryv.Auth.AuthStates.AUTHORIZED) {
        connection = new Pryv.Connection(state.apiEndpoint);
        logToConsole('# Browser succeeded for user ' + connection.apiEndpoint);
      }
      if (state.id === Pryv.Auth.AuthStates.SIGNOUT) {
        connection = null;
        logToConsole('# Logout');
      }
  }
    var serviceInfoUrl = 'https://api.pryv.com/lib-js/demos/service-info.json';
    (async function () {
      var service = await Pryv.Auth.setupAuth(authSettings, serviceInfoUrl);
    })();
  </script>
</body>
</html>

Fetch access info

Implementation of access info.

const apiEndpoint = 'https://ck6bwmcar00041ep87c8ujf90@drtom.pryv.me';
const connection = new Pryv.Connection(apiEndpoint);
const accessInfo = await connection.accessInfo();

Using Service.login() (trusted apps only)

auth.login reference

const serviceInfoUrl = 'https://reg.pryv.me/service/info';
const appId = 'lib-js-sample';
const service = new Pryv.Service(serviceInfoUrl);
const connection = await service.login(username, password, appId);

API calls

Api calls are based on the batch call specifications: Call batch API reference

const apiCalls = [
  {
    "method": "streams.create",
    "params": { "id": "heart", "name": "Heart" }
  },
  {
    "method": "events.create",
    "params": { "time": 1385046854.282, "streamId": "heart", "type": "frequency/bpm", "content": 90 }
  },
  {
    "method": "events.create",
    "params": { "time": 1385046854.283, "streamId": "heart", "type": "frequency/bpm", "content": 120 }
  }
]

try {
  const result = await connection.api(apiCalls)
} catch (e) {
  // handle error
}

Advanced usage of API calls with optional individual result and progress callbacks

let count = 0;
// the following will be called on each API method result it was provided for
function handleResult(result) { console.log('Got result ' + count++ + ': ' + JSON.stringify(result)); }

function progress(percentage) { console.log('Processed: ' + percentage + '%'); }

const apiCalls = [
  {
    method: 'streams.create',
    params: { id: 'heart', name: 'Heart' }
  },
  {
    method: 'events.create',
    params: { time: 1385046854.282, streamId: 'heart', type: 'frequency/bpm', content: 90 },
    handleResult: handleResult
  },
  {
    method: 'events.create',
    params: { time: 1385046854.283, streamId: 'heart', type: 'frequency/bpm', content: 120 },
    handleResult: handleResult
  }
]

try {
  const result = await connection.api(apiCalls, progress)
} catch (e) {
  // handle error
}

Get Events Streamed

When events.get will provide a large result set, it is recommended to use a method that streams the result instead of the batch API call.

Pryv.Connection.getEventsStreamed() parses the response JSON as soon as data is available and calls the forEachEvent() callback on each event object.

The callback is meant to store the events data, as the function does not return the API call result, which could overflow memory in case of JSON deserialization of a very large data set. Instead, the function returns an events count and eventually event deletions count as well as the common metadata.

Example:

const now = (new Date()).getTime() / 1000;
const queryParams = { fromTime: 0, toTime: now, limit: 10000};
const events = [];
function forEachEvent(event) {
  events.push(event);
}

try {
  const result = await connection.getEventsStreamed(queryParams, forEachEvent);
} catch (e) {
  // handle error
}

result:

{
  eventsCount: 10000,
  meta:
  {
      apiVersion: '1.4.26',
      serverTime: 1580728336.864,
      serial: '2019061301'
  }
}

Example with Includes deletion:

const now = (new Date()).getTime() / 1000;
const queryParams = { fromTime: 0, toTime: now, includeDeletions: true, modifiedSince: 0};
const events = [];
function forEachEvent(event) {
  events.push(event);
  // events with .deleted or/and .trashed properties can be tracked here
}

try {
  const result = await connection.getEventsStreamed(queryParams, forEachEvent);
} catch (e) {
  // handle error
}

result:

{
  eventDeletionsCount: 150,
  eventsCount: 10000,
  meta:
  {
      apiVersion: '1.4.26',
      serverTime: 1580728336.864,
      serial: '2019061301'
  }
}

Events with Attachments

This shortcut allows to create an event with an attachment in a single API call.

Node.js

const filePath = './test/my_image.png';
const result = await connection.createEventWithFile(
  {
    type: 'picture/attached',
    streamId: 'data'
  },
  filePath
);

or from a Buffer

const filePath = './test/my_image.png';
const bufferData = fs.readFileSync(filePath);

const result = await connection.createEventWithFileFromBuffer(
  {
    type: 'picture/attached',
    streamId: 'data'
  },
  bufferData,
  'my_image.png' // filename
);

Browser

From an Input field

<input type="file" id="file-upload"><button onClick='uploadFile()'>Save Value</button>

<script>
  var formData = new FormData();
  formData.append(
    'file0',
    document.getElementById('create-file').files[0]
) ;

  connection.createEventWithFormData(
    {
      type: 'file/attached',
      streamId: 'test'
    },
    formData)
    .then(function (res, err) {
      // handle result here
    }
  );
</script>

Progamatically created content:

var formData = new FormData();
var blob = new Blob(
  ['Hello'],
  { type: "text/txt" }
);
formData.append("webmasterfile", blob);

connect.createEventWithFormData(
  {
    type: 'file/attached',
    streamId: 'data'
  },
  formData)
  .then(function (res, err) {
    // handle result here
  }
);

// -- alternative with a filename

connect.createEventWithFileFromBuffer(
  {
    type: 'file/attached',
    streamId: 'data'
  },
  blob, 'filename.txt')  // here we can directly use the blob
  .then(function (res, err) {
    // handle result here
  }
);


High Frequency Events

Reference: https://api.pryv.com/reference/#hf-events

function generateSerie() {
  const serie = [];
  for (let t = 0; t < 100000, t++) { // t will be the deltaTime in seconds
    serie.push([t, Math.sin(t/1000)]);
  }
  return serie;
}
const pointsA = generateSerie();
const pointsB = generateSerie();

function postHFData(points) { // must return a Promise
   return async function (result) { // will be called each time an HF event is created
    return await connection.addPointsToHFEvent(result.event.id, ['deltaTime', 'value'], points);
  }
}

const apiCalls = [
  {
    method: 'streams.create',
    params: { id: 'signal1', name: 'Signal1' }
  },
  {
    method: 'streams.create',
    params: { id: 'signal2', name: 'Signal2' }
  },
  {
    method: 'events.create',
    params: { streamId: 'signal1', type: 'serie:frequency/bpm' },
    handleResult: postHFData(pointsA)
  },
  {
    method: 'events.create',
    params: { streamId: 'signal2', type: 'serie:frequency/bpm' },
    handleResult: postHFData(pointsB)
  }
]

try {
  const result = await connection.api(apiCalls)
} catch (e) {
  // handle error
}

Service Information and assets

A Pryv.io deployment is a unique "Service", as an example Pryv Lab is a service, deployed on the pryv.me domain name.

It relies on the content of a service information configuration, See: Service Information API reference

Pryv.Service

Exposes tools to interact with Pryv.io at a "Platform" level.

Initizalization with a service info URL
const service = new Pryv.Service('https://reg.pryv.me/service/info');
Initialization with the content of a service info configuration

Service information properties can be overriden with specific values. This might be useful to test new designs on production platforms.

const serviceInfoUrl = 'https://reg.pryv.me/service/info';
const serviceCustomizations = {
  name: 'Pryv Lab 2',
  assets: {
    definitions: 'https://pryv.github.io/assets-pryv.me/index.json'
  }
}
const service = new Pryv.Service(serviceInfoUrl, serviceCustomizations);
Usage of Pryv.Service.

See: Pryv.Service for more details

  • service.info() - returns the content of the serviceInfo in a Promise

    // example: get the name of the platform
    const serviceName = await service.info().name
    
  • service.infoSync(): returns the cached content of the serviceInfo, requires service.info() to be called first.

  • service.apiEndpointFor(username, token) Will return the corresponding API endpoint for the provided credentials, token can be omitted.

Pryv.Browser & Visual assets

Pryv.Browser - retrieve serviceInfo from query URL

A single Web App might need to be run on different Pryv.io platforms. This is the case of most Pryv.io demonstrators.

The corresponding Pryv.io platform can be specified by providing the Service Information URL as query parameter pryvServiceInfoUrl as per the Pryv App Guidelines. It can be extracted using Pryv.Browser.serviceInfoFromUrl() .

Example of usage for web App with the url https://api.pryv.com/app-web-access/?pryvServiceInfoUrl=https://reg.pryv.me/service/info

let defaultServiceInfoUrl = 'https://reg.pryv.me/service/info';
// if present override serviceInfoURL from URL query param "?pryvServiceInfoUrl=.."
serviceInfoUrl = Pryv.Browser.serviceInfoFromUrl() || defaultServiceInfoUrl;

(async function () {
    var service = await Pryv.Auth.setupAuth(authSettings, serviceInfoUrl, serviceCustomizations);
})();

Visual assets

To customize assets and visuals refer to: pryv.me assets github

To customize the Sign in Button refer to: sign in button in pryv.me assets

(service.assets()).setAllDefaults(): loads the css and favicon properties of assets definitions.

(async function () {
  const service = await Pryv.Auth.setupAuth(authSettings, serviceInfoUrl);
  (await service.assets()).setAllDefaults(); // will load the default Favicon and CSS for this platform
})();

Customize Auth process

You can customize the authentication process at different levels:

  1. Using a custom login button to launch the Pryv.io Authentication process.
  2. Using a custom UI for the Pryv.io Authentication process, including the flow of app-web-auth3.

Using a custom login button

You will need to implement a class that instanciates an AuthController object and implements a few methods. We will go through this guide using the Browser's default Login Button provided with this library as example.

Initialization

You should provide it authSettings (See Obtain a Pryv.Connection) and an instance of Service at initialization. As this phase might contain asynchronous calls, we like to split it between the constructor and an async init() function. In particular, you will need to instanciate an AuthController object.

constructor(authSettings, service) {
  this.authSettings = authSettings;
  this.service = service;
  this.serviceInfo = service.infoSync();
}

async init () {
  // initialize button visuals
  // ...

  // set cookie key for authorization data - browser only
  this._cookieKey = 'pryv-libjs-' + this.authSettings.authRequest.requestingAppId;

  // initialize controller
  this.auth = new AuthController(this.authSettings, this.service, this);
  await this.auth.init();
}
Authorization data

At initialization, the AuthController will attempt to fetch some persisted authorization credentials, using LoginButton.getAuthorizationData(). In the browser case, we are using a client-side cookie. For other frameworks, use an appropriate secure storage.

getAuthorizationData () {
  return Cookies.get(this._cookieKey);
}
Authentication lifecycle

The authentication process implementation on the frontend is defined in the following states:

  1. Loading: while the visual assets are loading
  2. Initialized: visuals assets are loaded, or when polling concludes with Result: Refused
  3. Need sign in: From the response of the auth request through polling
  4. Authorized: When polling concludes with Result: Accepted
  5. Sign out: When the user triggers a deletion of the client-side authorization credentials, usually by clicking the button after being signed in
  6. Error: See message for more information

You will need to provide a function to act depending on the state. The statesNEED_SIGNIN and AUTHORIZED contain the same fields as the auth process polling responses. LOADING, INITIALIZED and SIGNOUT only contain status. The ERROR state carries a message property.

async onStateChange (state) {
  switch (state.status) {
    case AuthStates.LOADING:
      this.text = getLoadingMessage(this);
      break;
    case AuthStates.INITIALIZED:
      this.text = getInitializedMessage(this, this.serviceInfo.name);
      break;
    case AuthStates.NEED_SIGNIN:
      const loginUrl = state.authUrl || state.url; // .url is deprecated
      if (this.authSettings.authRequest.returnURL) { // open on same page (no Popup)
        location.href = loginUrl;
        return;
      } else {
        startLoginScreen(this, loginUrl);
      }
      break;
    case AuthStates.AUTHORIZED:
      this.text = state.username;
      this.saveAuthorizationData({
        apiEndpoint: state.apiEndpoint,
        username: state.username
      });
      break;
    case AuthStates.SIGNOUT:
      const message = this.messages.SIGNOUT_CONFIRM ? this.messages.SIGNOUT_CONFIRM : 'Logout ?';
      if (confirm(message)) {
        this.deleteAuthorizationData();
        this.auth.init();
      }
      break;
    case AuthStates.ERROR:
      this.text = getErrorMessage(this, state.message);
      break;
    default:
      console.log('WARNING Unhandled state for Login: ' + state.status);
  }
  if (this.loginButtonText) {
    this.loginButtonText.innerHTML = this.text;
  }
}
Button actions

The button actions should be handled by the AuthController in the following way:

// LoginButton.js
onClick () {
  this.auth.handleClick();
}
// AuthController.js
async handleClick () {
  if (isAuthorized.call(this)) {
    this.state = { status: AuthStates.SIGNOUT };
  } else if (isInitialized.call(this)) {
    this.startAuthRequest();
  } else if (isNeedSignIn.call(this)) {
    // reopen popup
    this.state = this.state;
  } else {
    console.log('Unhandled action in "handleClick()" for status:', this.state.status);
  }
}
Custom button usage

You must then provide this class as following:

let service = await Pryv.Auth.setupAuth(
  authSettings, // See https://github.com/pryv/lib-js#within-a-webpage-with-a-login-button
  serviceInfoUrl,
  serviceCustomizations,
  MyLoginButton,
);

You will find a working example in ./web-demos/custom-login-button.html. You can run this code at https://api.pryv.com/lib-js/demos/custom-login-button.html.

Follow the instructions below on how to run these examples locally.

For a more advanced scenario, you can check the default button implementation at ./src/Browser/LoginButton.js.

Redirect user to the authentication page

There is a possibility that you would like to register the user in another page. You can check the ./web-demos/auth-with-redirection.html example. Also you can try the same code in https://api.pryv.com/lib-js/demos/auth-with-redirection.html. Here is the explanation how to launch web-demos locally

Launch web demos locally

You can find html examples in the ./web-demos directory. You can launch them in 2 ways:

  1. using rec-la that allows to run your code with a valid SSL certificate (this requires to have run npm run build prior). To launch the server you simply need to run:

    npm run webserver
    

    and open an example with the following URL https://l.rec.la:9443/demos/EXAMPLE_NAME.html**, like: https://l.rec.la:9443/demos/auth.html

  2. as a simple html file (service information must be passed as JSON to avoid CORS problem).

Change Log

2.2.0

  • Added TypeScript typings – contribution from @ovesco

2.1.7

  • Removed JSDOC dev dependency for security reason

2.1.0

  • UI separated from the Authentication logic
  • Extendable UI feature was added

2.0.3

  • Added Connection.username()
  • Various dependencies upgrades
  • Fixing Origin header in Browser distribution

2.0.1 Initial Release