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;