import Transport from './transport.js';
import Session from './session.js';
import Registry from './registry.js';
import {
where, field, param, textSearch
} from './query.js';
/**
* @typedef {Object} ClientOptions
* Options for the Client.
* @property {string} directory - The directory to pass through to {@link Session}.
* @property {string} registryUri - The registryUri to pass through to {@link Registry}.
* @property {string?} identityUri - Optional identityUri to pass through to {@link Session}.
* @property {string?} token - Token to authenticate with.
* @property {string?} tenant - Tenant to initialize with.
* @property {boolean?} enableCache - Indicates whether to cache service descriptors in {@link Registry}.
* @property {encodeUri?} encodeURI - Indicates whether to encode URIs before requesting in {@link Transport}.
* Will not re-encode existing sequences (e.g. `%20` will stay as `%20`, but `%2` will encode to `%202`)
*/
/**
* Provides initial setup and subsequent access to the SDK
*
* @param {ClientOptions} options - Options for the Client.
*
* @property {Transport} transport - a stored transport instance used for fetching
* @property {Session} session - a stored session to query the current users' authentication state with
* @property {Registry} registry - a stored registry reference to query
*
* @requires {@link Transport}
* @requires {@link Session}
* @requires {@link Registry}
*
* @tutorial getting_started
*
* @example
*
* import Client from '@pikselpalette/sequoia-js-client-sdk/lib/client';
* import { where, field } from '@pikselpalette/sequoia-js-client-sdk/lib/query';
*
* // Create a client:
* const client = new Client({ directory: 'piksel',
* registry: 'https://registry-sandbox.sequoia.piksel.com' });
*
* client.login('username', 'password').then(session => {
* // You can now query the session provided as the first argument (or
* // client.session); e.g. `session.isActive()`
*
* // Get a service::
* client.service('metadata').then(service => {
* // Get a resourceful endpoint (this is synchronous as the service passed
* // all the necessary data):
* const contents = service.resourcefulEndpoint('contents');
*
* contents.browse(where().fields('title', 'mediumSynopsis','duration', 'ref')
* .include('assets').page(1).perPage(24).orderByUpdatedAt().desc().count())
* .then(json => {
* // Do something with the json returned
* });
* });
* }).catch(error => {
* // Not logged in, inspect `error` to see why
* });
*
* @example
*
* // Adding a tenant argument to the Client means you can skip setting the tenant later on.
* const client = new Client({ directory: 'piksel',
* registry: 'https://registry-sandbox.sequoia.piksel.com',
* tenant: 'demo' });
*
* @example
*
* // Adding a token argument to the Client means you do not need to call generate()
* // in a separate step.
* const client = new Client({ directory: 'piksel',
* registry: 'https://registry-sandbox.sequoia.piksel.com',
* token: 'yourGeneratedToken' });
*/
class Client {
constructor({
directory,
registryUri,
identityUri,
token,
tenant,
enableCache,
encodeUri
}) {
this.transport = new Transport({}, encodeUri);
this.registry = new Registry(this.transport, registryUri, enableCache);
this.session = new Session(
this.transport,
directory,
this.registry,
identityUri
);
if (tenant) this.setTenancy(tenant);
if (token) this.generate(token);
}
/**
* Get a {@link ServiceDescriptor} from the {@link Registry}
*
* @deprecated Deprecated since 1.2.0. Use {@link Client#serviceDescriptors}
*
* @see {@link Registry#getService}
*
* @returns {Promise}
*/
service(serviceName) {
console.warn(`client.service() is deprecated as it is passing a serviceDescriptor and not a service.
Please use client.serviceDescriptors() instead`);
return this.serviceDescriptors(serviceName).then(([result]) => result);
}
/**
* Get a list of {@link ServiceDescriptor}s from the service endpoint
*
* @see {@link Registry#getServiceDescriptors}
*
* @param {...string} serviceNames - service names
*
* @returns {Promise}
*/
serviceDescriptors(...serviceNames) {
return this.registry.getServiceDescriptors(...serviceNames);
}
/**
* Get a list of {@link ServiceDescriptor}s from the SDK cache, falling back to the service endpoint
*
* @see {@link Registry#getCachedServiceDescriptors}
*
* @param {...string} serviceNames - service names
*
* @returns {Promise}
*/
cachedServiceDescriptors(...serviceNames) {
return this.registry.getCachedServiceDescriptors(...serviceNames);
}
/**
* Log an end user in with username and password credentials
*
* @param {string?} username - the end user's username
* @param {string?} password - the end user's password
* @param {AuthenticationOptions?} options
*
* @example
* // Standard 'pauth' login
* client.login('test_username', 'test_password').then((session) => {
* // Do something with the session
* });
*
* // 'pauth' style login to a custom endpoint
* client.login('test_username', 'test_password', {
* url: 'https://example.com/custom/pauth'
* }).then((session) => {
* // Custom url should return a json response of { 'access_token': <token> }
* // Do something with the session
* });
* @example
* // Standard 'oauth' login (password grant)
* const secret = 'somebase64secret==';
* client.login('test_username', 'test_password', {
* strategy: 'oauth',
* secret
* }).then((session) => {
* // Do something with the session
* });
*
* @example
* // Client oauth (client credentials grant)
* const secret = 'somebase64secret==';
* client.login(null, null, {
* strategy: 'oauth',
* secret
* }).then((session) => {
* // Do something with the session
* });
*
* @example
* // login will reject when not passing a secret
* client.login('test_username', 'test_password', {
* strategy: 'oauth'
* }).catch((err) => {
* // Inspect `err`
* });
*
* @see {Session#authenticateWithCredentials}
*
* @returns {Promise} - First argument to the resolved Promise is the {@link Session} object that
* has been updatedc
*/
login(username, password, options = { strategy: 'pauth' }) {
return this.session
.authenticateWithCredentials(username, password, options)
.then(session => this.registry.fetch(session.currentOwner()))
.then(() => this.session);
}
/**
* Generate a Session from an existing bearer token.
*
* It is also useful to use this if you acquire an access token via other means,
* i.e. an existing oauth mechanism for Sequoia
*
* Call this method without a token parameter to instantiate the client for anonymous
* usage. Note: currently the Sequoia registry does not provide anonymous access.
* See the below example for how to handle this currently.
*
* @param {string?} token - an existing bearer token for an end user
*
* @example
* client.generate('some token').then(doSomething);
*
* @example
* // Anonymous usage:
* client.generate().catch((err) => {
* if (err.response && err.response.status === 401) {
* client.registry.tenant = SQ_DIRECTORY;
*
* client.registry.services.push({
* owner: 'root',
* name: 'identity',
* title: 'Identity Service',
* location: SQ_IDENTITY_URL
* });
*
* client.registry.services.push({
* owner: 'root',
* name: 'gateway',
* title: 'Gateway Service',
* location: client.registry.registryUri.replace('registry', 'gateway')
* });
* }
* }).then(doSomething);
*
* @returns {Promise} - First argument to the resolved Promise is the `Session` object that
* has been updated
*/
generate(token) {
const p = this.session.authenticateWithToken(token);
return p
.then(session => this.registry.fetch(session.currentOwner()))
.then(() => this.session);
}
/**
* Changes the password of a user
*
* @returns null
*/
changePassword(username, oldPassword, newPassword) {
return this.session.changePassword(username, oldPassword, newPassword);
}
/**
* Reset the password of a user
*
* @returns Object
*/
resetPassword(username) {
return this.session.resetPassword(username);
}
/**
* Log out an end user
*
* @returns {Session}
*/
logout() {
return this.session.destroy();
}
/**
* Set the current tenancy for the user
*
* When switching tenancies, [this.registry]{@link Registry} will be
* repopulated with the services available in that tenancy.
*
* Note: existing instances of {@link ServiceDescriptor}s, {@link ResourcefulEndpoint}s etc
* will not have the 'owner' updated when switching to a new tenancy. See the below
* example for more info.
*
* @param {string} tenantName - the name of the tenancy to use
*
* @example <caption>Switching a tenancy</caption>
* await client.generate(some_token);
* await client.setTenancy('test');
*
* let identity = await client.service('identity');
* let usersEndpoint = identity.resourcefulEndpoint('users');
* await usersEndpoint.browse() // https://<endpoint>/data/users?owner=test
*
* await client.setTenancy('production');
* // At this point `identity` and `usersEndpoint` will still be doing
* // `fetch`es with `?owner=test`. You will need to repopulate them
* // as below
* await usersEndpoint.browse() // https://<endpoint>/data/users?owner=test
*
* identity = await client.service('identity');
* usersEndpoint = identity.resourcefulEndpoint('users');
* await usersEndpoint.browse() // https://<endpoint>/data/users?owner=production
*
* @returns {Promise<Session>}
*/
setTenancy(tenantName) {
// If the tenants.length === 0, it indicated that we are logging in
// as an anonymous user, where the tenants would not have been set.
if (this.session.tenants.length > 0) {
const tenantIds = this.session.tenants.map(item => item.name);
if (!tenantIds.some(n => n === tenantName)) {
return Promise.reject(new Error('Tenant does not exist'));
}
}
this.session.currentTenant = tenantName;
// Switching a tenancy whilst we've already logged in requires us
// to update the registry
return this.registry
.fetch(tenantName)
.then(() => this.session.populateAccess())
.then(() => this.session);
}
/**
* Set the current directory
*
* This will only affect new authentications,
* if the client is already authenticated, it will not do anything.
*
*/
setDirectory(directory) {
this.session.directory = directory;
}
/**
* Set callback for when the Token is about to expire,
* will be called before expiry based on the provided threshold
*
* Call with null to cancel the callback.
*
* @param {Function} callback Will be called with the current [session.access]{@link Session}
* @param {Number} threshold Number of milliseconds _before_ expiry when callback will be invoked. Defaults to 60000 (1 minute)
*/
onExpiryWarning(callback, threshold = 60000) {
this.session.setOnExpiryWarning(callback, threshold);
}
}
// Export the query methods for use in non-es6 module environments.
// e.g.
// const Client = require('@pikselpalette/sequoia-js-client-sdk/dist/sequoia-client.js');
// const { where, field, param, textSearch } = Client;
Client.where = where;
Client.field = field;
Client.param = param;
Client.textSearch = textSearch;
export default Client;