Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multitenancy support #141

Merged
merged 27 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c98707e
Add the notion of tenant provider to the client
dmitrykuzmin Apr 29, 2020
f43fab9
Use the tenant provider in the `ActorRequestFactory`
dmitrykuzmin Apr 30, 2020
b8e74d4
Add tests for the multitenancy-related classes
dmitrykuzmin Apr 30, 2020
f5eba2d
Make integration tests use multitenant context and client
dmitrykuzmin Apr 30, 2020
2c89a9f
Add basic single-tenant tests to be executed along with multitenant ones
dmitrykuzmin Apr 30, 2020
6c132af
Fix the tests
dmitrykuzmin May 3, 2020
3cd023b
Increase the timeout on single-tenant client tests
dmitrykuzmin May 3, 2020
92250b7
Make the `TenantProvider` checks stricter
dmitrykuzmin May 3, 2020
4a02628
Further increase the timeout on single-tenant client tests
dmitrykuzmin May 3, 2020
eb2717c
Simplify the single-tenant query test
dmitrykuzmin May 4, 2020
33f0d0e
Fix a typo
dmitrykuzmin May 4, 2020
562b335
Make the timeout in a query test smaller
dmitrykuzmin May 4, 2020
eca3dd4
Get rid of the semicolons in doc
dmitrykuzmin May 4, 2020
d6d7824
Fix grammar
dmitrykuzmin May 4, 2020
058e85a
Make test model entities package-private and remove unnecessary c-tors
dmitrykuzmin May 4, 2020
24921eb
Fix a typo
dmitrykuzmin May 4, 2020
c1491e5
Set the Spine version to `1.5.4`
dmitrykuzmin May 4, 2020
f702289
Remove a redundant empty line
dmitrykuzmin May 5, 2020
96390cf
Make all `test-app` repos `package-private` and `final`
dmitrykuzmin May 5, 2020
6159a41
Keep the version in sync in `package.json` files and the license report
dmitrykuzmin May 5, 2020
8975799
Add more shortcuts for the `TenantId` creation
dmitrykuzmin May 5, 2020
c12a2f2
Use the default repositories in tests
dmitrykuzmin May 5, 2020
143f1dd
Update the `config`
dmitrykuzmin May 5, 2020
c074f47
Update the license report
dmitrykuzmin May 5, 2020
e253663
Fix the single-tenant query test
dmitrykuzmin May 5, 2020
d4eb0d1
Make the timeout smaller
dmitrykuzmin May 5, 2020
19a8aa2
Fix a typo
dmitrykuzmin May 5, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have convenience methods for other types of TennantIds as well.

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