Skip to content

Commit

Permalink
Merge pull request #141 from SpineEventEngine/multitenancy-support
Browse files Browse the repository at this point in the history
Multitenancy support
  • Loading branch information
dmitrykuzmin authored May 5, 2020
2 parents 895274c + 19a8aa2 commit 6b4d32c
Show file tree
Hide file tree
Showing 36 changed files with 1,114 additions and 610 deletions.
32 changes: 28 additions & 4 deletions client-js/main/client/actor-request-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@
import uuid from 'uuid';

import {Message} from 'google-protobuf';
import {FieldMask} from '../proto/google/protobuf/field_mask_pb';
import {Timestamp} from '../proto/google/protobuf/timestamp_pb';
import {OrderBy, Query, QueryId, ResponseFormat} from '../proto/spine/client/query_pb';
import {Topic, TopicId} from '../proto/spine/client/subscription_pb';
import {
CompositeFilter,
Filter,
IdFilter,
Target,
TargetFilters
} from '../proto/spine/client/filters_pb';
import {OrderBy, Query, QueryId, ResponseFormat} from '../proto/spine/client/query_pb';
import {Topic, TopicId} from '../proto/spine/client/subscription_pb';
import {ActorContext} from '../proto/spine/core/actor_context_pb';
import {Command, CommandContext, CommandId} from '../proto/spine/core/command_pb';
import {UserId} from '../proto/spine/core/user_id_pb';
import {ZoneId, ZoneOffset} from '../proto/spine/time/time_pb';
import {FieldMask} from '../proto/google/protobuf/field_mask_pb';
import {isProtobufMessage, Type, TypedMessage} from './typed-message';
import {AnyPacker} from './any-packer';
import {FieldPaths} from './field-paths';
Expand Down Expand Up @@ -1044,9 +1044,30 @@ export class ActorRequestFactory {
* Creates a new instance of ActorRequestFactory for the given actor.
*
* @param {!ActorProvider} actorProvider a provider of an actor
* @param {?TenantProvider} tenantProvider a provider of the current tenant, if omitted, the
* application is considered single-tenant
*/
constructor(actorProvider) {
constructor(actorProvider, tenantProvider) {
this._actorProvider = actorProvider;
this._tenantProvider = tenantProvider;
}

/**
* Creates a new `ActorRequestFactory` based on the passed options.
*
* @param {!ClientOptions} options the client initialization options
* @return {ActorRequestFactory} a new `ActorRequestFactory` instance
*/
static create(options) {
if (!options) {
throw new Error('Client options are not defined.')
}
const actorProvider = options.actorProvider;
if (!actorProvider) {
throw new Error('The actor provider should be set in the client options in order to ' +
'construct an `ActorRequestFactory`.');
}
return new ActorRequestFactory(actorProvider, options.tenantProvider);
}

/**
Expand Down Expand Up @@ -1081,6 +1102,9 @@ export class ActorRequestFactory {

_actorContext() {
const result = new ActorContext();
if (this._tenantProvider) {
result.setTenantId(this._tenantProvider.tenantId());
}
result.setActor(this._actorProvider.get());
const seconds = Math.round(new Date().getTime() / 1000);
const time = new Timestamp();
Expand Down
5 changes: 4 additions & 1 deletion client-js/main/client/client-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import {HttpEndpoint} from "./http-endpoint";
* the custom implementation of `Client`
* @property {?Routing} routing
* custom configuration of HTTP endpoints
* @property {?TenantProvider} tenantProvider
* the provider of an active tenant ID, if not specified, the application is considered
* single-tenant
*/

/**
Expand Down Expand Up @@ -105,7 +108,7 @@ export class AbstractClientFactory {
static createCommanding(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const requestFactory = new ActorRequestFactory(options.actorProvider);
const requestFactory = ActorRequestFactory.create(options);

return new CommandingClient(endpoint, requestFactory);
}
Expand Down
6 changes: 3 additions & 3 deletions client-js/main/client/direct-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class DirectClientFactory extends AbstractClientFactory {
static _clientFor(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const requestFactory = new ActorRequestFactory(options.actorProvider);
const requestFactory = ActorRequestFactory.create(options);

const querying = new DirectQueryingClient(endpoint, requestFactory);
const subscribing = new NoOpSubscribingClient(requestFactory);
Expand All @@ -58,13 +58,13 @@ export class DirectClientFactory extends AbstractClientFactory {
static createQuerying(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const requestFactory = new ActorRequestFactory(options.actorProvider);
const requestFactory = ActorRequestFactory.create(options);

return new DirectQueryingClient(endpoint, requestFactory);
}

static createSubscribing(options) {
const requestFactory = new ActorRequestFactory(options.actorProvider);
const requestFactory = ActorRequestFactory.create(options);
return new NoOpSubscribingClient(requestFactory);
}

Expand Down
6 changes: 3 additions & 3 deletions client-js/main/client/firebase-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export class FirebaseClientFactory extends AbstractClientFactory {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
const requestFactory = new ActorRequestFactory(options.actorProvider);
const requestFactory = ActorRequestFactory.create(options);
const subscriptionService = new FirebaseSubscriptionService(endpoint);

const querying = new FirebaseQueryingClient(endpoint, firebaseDatabaseClient, requestFactory);
Expand All @@ -345,7 +345,7 @@ export class FirebaseClientFactory extends AbstractClientFactory {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
const requestFactory = new ActorRequestFactory(options.actorProvider);
const requestFactory = ActorRequestFactory.create(options);

return new FirebaseQueryingClient(endpoint, firebaseDatabaseClient, requestFactory);
}
Expand All @@ -354,7 +354,7 @@ export class FirebaseClientFactory extends AbstractClientFactory {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
const requestFactory = new ActorRequestFactory(options.actorProvider);
const requestFactory = ActorRequestFactory.create(options);
const subscriptionService = new FirebaseSubscriptionService(endpoint);

return new FirebaseSubscribingClient(endpoint,
Expand Down
144 changes: 144 additions & 0 deletions client-js/main/client/tenant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright 2020, TeamDev. All rights reserved.
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import {TenantId} from "../proto/spine/core/tenant_id_pb";
import {EmailAddress} from "../proto/spine/net/email_address_pb";
import {InternetDomain} from "../proto/spine/net/internet_domain_pb";
import {isProtobufMessage, Type} from "./typed-message";

/**
* A factory of `TenantId` instances.
*
* Exposes methods that are "shortcuts" for the convenient creation of `TenantId` message
* instances.
*/
export class TenantIds {

/**
* Constructs a `TenantId` which represents an internet domain.
*
* @param {!string} domainName the domain name as a plain string
* @return {TenantId} a new `TenantId` instance
*/
static internetDomain(domainName) {
if (!domainName) {
throw new Error('Expected a non-empty internet domain name.');
}
const domain = new InternetDomain();
domain.setValue(domainName);
const result = new TenantId();
result.setDomain(domain);
return result;
}

/**
* Constructs a `TenantId` which represents an email address.
*
* @param {!string} address the email address as a plain string
* @return {TenantId} a new `TenantId` instance
*/
static emailAddress(address) {
if (!address) {
throw new Error('Expected a non-empty email address value.');
}
const emailAddress = new EmailAddress();
emailAddress.setValue(address);
const result = new TenantId();
result.setEmail(emailAddress);
return result;
}

/**
* Constructs a `TenantId` which is a plain string value.
*
* @param {!string} tenantIdValue the tenant ID
* @return {TenantId} a new `TenantId` instance
*/
static plainString(tenantIdValue) {
if (!tenantIdValue) {
throw new Error('Expected a valid tenant ID value.');
}
const result = new TenantId();
result.setValue(tenantIdValue);
return result;
}
}

/**
* The current tenant provider.
*
* This object is passed to the `ActorRequestFactory` and is used during creation of all
* client-side requests.
*
* The current tenant ID can be switched dynamically with the help of the `update` method.
*
* If it is necessary to update the current ID to a "no tenant" value (to work in a single-tenant
* environment), pass the default tenant ID to the `update` method as follows:
* `tenantProvider.update(new TenantId())`.
*/
export class TenantProvider {

/**
* Creates a new `TenantProvider` configured with the passed tenant ID.
*
* The argument may be omitted but until the `_tenantId` is assigned some non-default value, the
* application is considered single-tenant.
*
* @param {?TenantId} tenantId the ID of the currently active tenant
*/
constructor(tenantId) {
if (tenantId) {
TenantProvider._checkIsValidTenantId(tenantId);
this._tenantId = tenantId;
}
}

/**
* @param {!TenantId} tenantId the ID of the currently active tenant
*/
update(tenantId) {
TenantProvider._checkIsValidTenantId(tenantId);
this._tenantId = tenantId;
}

/**
* Returns the currently active tenant ID.
*
* @return {TenantId}
*/
tenantId() {
return this._tenantId;
}

/**
* Checks that the passed object represents a `TenantId` message.
*
* @param {!object} tenantId the object to check
* @private
*/
static _checkIsValidTenantId(tenantId) {
if (!tenantId
|| !isProtobufMessage(tenantId)
|| Type.forMessage(tenantId).url().value() !== 'type.spine.io/spine.core.TenantId') {
throw new Error(`Expected a valid instance of the 'TenantId' message.`
+ `The '${tenantId}' was passed instead.`);
}
}
}
1 change: 1 addition & 0 deletions client-js/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export {FirebaseDatabaseClient} from './client/firebase-database-client';
export {HttpClient} from './client/http-client';
export {Client} from './client/client';
export {init} from './client/spine';
export {TenantIds, TenantProvider} from './client/tenant';
export * from './client/errors';
2 changes: 1 addition & 1 deletion client-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "spine-web",
"version": "1.5.3",
"version": "1.5.4",
"license": "Apache-2.0",
"description": "A JS client for interacting with Spine applications.",
"homepage": "https://spine.io",
Expand Down
82 changes: 82 additions & 0 deletions client-js/test/client/tenant-ids-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2020, TeamDev. All rights reserved.
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import assert from 'assert';
import {TenantIds} from "../../main/client/tenant";

describe('TenantIds', function () {

it('create a tenant ID which represents an internet domain', done => {
const internetDomain = "en.wikipedia.org";
const tenantId = TenantIds.internetDomain(internetDomain);
assert.equal(tenantId.getDomain(), internetDomain);
done();
});

it('throws an `Error` when the passed internet domain name is not defined', () => {
assert.throws(
() => TenantIds.internetDomain(undefined)
);
});

it('throws an `Error` when the passed internet domain name is empty', () => {
assert.throws(
() => TenantIds.internetDomain('')
);
});

it('create a tenant ID which represents an email address', done => {
const emailAddress = "user@test.com";
const tenantId = TenantIds.emailAddress(emailAddress);
assert.equal(tenantId.getEmail(), emailAddress);
done();
});

it('throws an `Error` when the passed email address value is not defined', () => {
assert.throws(
() => TenantIds.emailAddress(undefined)
);
});

it('throws an `Error` when the passed email address value is empty', () => {
assert.throws(
() => TenantIds.emailAddress('')
);
});

it('create a plain string tenant ID', done => {
const tenantIdValue = "some-tenant-ID";
const tenantId = TenantIds.plainString(tenantIdValue);
assert.equal(tenantId.getValue(), tenantIdValue);
done();
});

it('throws an `Error` when the passed string is not defined', () => {
assert.throws(
() => TenantIds.plainString(undefined)
);
});

it('throws an `Error` when the passed string is empty', () => {
assert.throws(
() => TenantIds.plainString('')
);
});
});
Loading

0 comments on commit 6b4d32c

Please sign in to comment.