Skip to content

Commit 1bbeaa7

Browse files
authored
Merge pull request #121 from dolittle/sync-resources
Fetch resources when connecting, bind MongoDB types in DI container, and expose a Task to wait for the client to be Connected
2 parents f80bc9f + 5fc3301 commit 1bbeaa7

24 files changed

+371
-237
lines changed

Samples/MongoDBProjections/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DolittleClient } from '@dolittle/sdk';
77
import { TenantId } from '@dolittle/sdk.execution';
88
import { setTimeout } from 'timers/promises';
99
import { DateTime } from 'luxon';
10+
import { Collection } from 'mongodb';
1011

1112
import { DishCounter } from './DishCounter';
1213
import { DishPrepared } from './DishPrepared';
@@ -25,8 +26,7 @@ import { DishPrepared } from './DishPrepared';
2526

2627
await setTimeout(1000);
2728

28-
const db = await client.resources.forTenant(TenantId.development).mongoDB.getDatabase();
29-
const dishCounterCollection = db.collection(DishCounter);
29+
const dishCounterCollection = client.services.forTenant(TenantId.development).get(Collection.forReadModel(DishCounter));
3030

3131
for (const { name, numberOfTimesPrepared, lastPrepared } of await dishCounterCollection.find().toArray()) {
3232
client.logger.info(`The kitchen has prepared ${name} ${numberOfTimesPrepared} times. The last time was ${lastPrepared}`);

Source/projections/Builders/ProjectionsModelBuilder.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Dolittle. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
import { Db, Collection } from 'mongodb';
5+
46
import { IClientBuildResults, IModel } from '@dolittle/sdk.common';
57
import { IServiceProviderBuilder } from '@dolittle/sdk.dependencyinversion';
68
import { IEventTypes } from '@dolittle/sdk.events';
9+
import '@dolittle/sdk.resources';
710

811
import { ProjectionProcessor } from '../Internal/ProjectionProcessor';
912
import { IProjectionReadModelTypes } from '../Store/IProjectionReadModelTypes';
@@ -44,6 +47,12 @@ export class ProjectionsModelBuilder {
4447
const projection = processorBuilder.build(this._eventTypes, this._buildResults);
4548
if (projection !== undefined) {
4649
processors.push(new ProjectionProcessor(projection, this._eventTypes));
50+
51+
if (projection.copies.mongoDB.shouldCopyToMongoDB && projection.readModelType !== undefined) {
52+
this._bindings.addTenantServices(binder => {
53+
binder.bind(Collection.forReadModel(projection.readModelType!)).toFactory(services => services.get(Db).collection(projection.copies.mongoDB.collectionName.value));
54+
});
55+
}
4756
}
4857
}
4958

Source/projections/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@
5656
"@dolittle/sdk.execution": "22.2.0",
5757
"@dolittle/sdk.protobuf": "22.2.0",
5858
"@dolittle/sdk.resilience": "22.2.0",
59+
"@dolittle/sdk.resources": "22.2.0",
5960
"@dolittle/sdk.services": "22.2.0",
6061
"@dolittle/types": "6.0.0",
6162
"luxon": "1.24.1",
62-
"@types/luxon": "1.24.1"
63+
"mongodb": "4.1.4"
6364
},
64-
"devDependencies": {}
65-
}
65+
"devDependencies": {
66+
"@types/luxon": "1.24.1"
67+
}
68+
}

Source/resources/FailedToGetResource.ts renamed to Source/resources/FailedToGetResourceForTenant.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@
33

44
import { Exception } from '@dolittle/rudiments';
55
import { TenantId } from '@dolittle/sdk.execution';
6+
67
import { ResourceName } from './ResourceName';
78

89
/**
910
* Exception that gets thrown when getting a resource for a tenant failed.
1011
*/
11-
export class FailedToGetResource extends Exception {
12+
export class FailedToGetResourceForTenant extends Exception {
1213
/**
13-
* Initializes a new instance of the {@link FailedToGetResource} class.
14-
* @param {ResourceName} resourceName - The resource name that was attempted to get.
15-
* @param {TenantId} tenant - The tenant that the resource was attemted to get for.
14+
* Initializes a new instance of the {@link FailedToGetResourceForTenant} class.
15+
* @param {ResourceName} resource - The resource name that was attempted to get.
16+
* @param {TenantId} tenant - The tenant that the resource was attempted to get for.
1617
* @param {string} reason - The reason for the failure.
1718
*/
18-
constructor(resourceName: ResourceName, tenant: TenantId, reason: string) {
19-
super(`Failed to get resource ${resourceName.value} for tenant ${tenant.value} because ${reason}`);
19+
constructor(resource: ResourceName, tenant: TenantId, reason: string) {
20+
super(`Failed to get resource ${resource} for tenant ${tenant} because ${reason}`);
2021
}
2122
}

Source/resources/IResource.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Dolittle. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { Tenant } from '@dolittle/sdk.tenancy';
5+
import { Cancellation } from '@dolittle/sdk.resilience';
6+
7+
import { IResourcesBuilder } from '../IResourcesBuilder';
8+
9+
/**
10+
* Defines a system that can create instances of {@link IResourcesBuilder} by fetching resources from the Runtime.
11+
*/
12+
export abstract class IFetchResources {
13+
/**
14+
* Creates a {@link IResourcesBuilder} by fetching resources for the provided tenants.
15+
* @param {Tenant[]} tenants - The tenants to fetch resources for.
16+
* @param {Cancellation} [cancellation] - An optional cancellation to cancel the operation.
17+
* @returns {Promise<IResourcesBuilder>} A {@link Promise} that, when resolved, returns the {@link IResourcesBuilder}.
18+
*/
19+
abstract fetchResourcesFor(tenants: readonly Tenant[], cancellation?: Cancellation): Promise<IResourcesBuilder>;
20+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) Dolittle. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { Logger } from 'winston';
5+
6+
import { ExecutionContext, TenantId } from '@dolittle/sdk.execution';
7+
import { ExecutionContexts } from '@dolittle/sdk.protobuf';
8+
import { Cancellation } from '@dolittle/sdk.resilience';
9+
import { reactiveUnary, UnaryMethod } from '@dolittle/sdk.services';
10+
11+
import { Failure } from '@dolittle/contracts/Protobuf/Failure_pb';
12+
import { CallRequestContext } from '@dolittle/contracts/Services/CallContext_pb';
13+
import { ResourcesClient } from '@dolittle/runtime.contracts/Resources/Resources_grpc_pb';
14+
15+
import { FailedToGetResourceForTenant } from '../FailedToGetResourceForTenant';
16+
import { ResourceName } from '../ResourceName';
17+
18+
/**
19+
* Represents a system that can create resources by fetching configuration from the Runtime.
20+
* @template TResource - The type of the resource.
21+
* @template TRequest - The type of the resource configuration request.
22+
* @template TResponse - The type of the resource configuration response.
23+
*/
24+
export abstract class ResourceCreator<TResource, TRequest, TResponse> {
25+
26+
/**
27+
* Initialises a new instance of the {@link ResourceCreator} class.
28+
* @param {ResourceName} _resource - The name of the resource type.
29+
* @param {UnaryMethod} _method - The gRPC method to call to get the resource configuration from the Runtime.
30+
* @param {ResourcesClient} _client - The resources client to make requests to the Runtime with.
31+
* @param {ExecutionContext} _executionContext - The base execution context for the client.
32+
* @param {Logger} _logger - The logger to use for logging.
33+
*/
34+
protected constructor(
35+
private readonly _resource: ResourceName,
36+
private readonly _method: UnaryMethod<TRequest, TResponse>,
37+
private readonly _client: ResourcesClient,
38+
private readonly _executionContext: ExecutionContext,
39+
private readonly _logger: Logger,
40+
) {}
41+
42+
/**
43+
* Creates the resource for the provided tenant by fetching configuration from the Runtime.
44+
* @param {TenantId} tenant - The tenant id to create the resource for.
45+
* @param {Cancellation} cancellation - An optional cancellation to cancel the operation.
46+
* @returns {Promise} - A {@link Promise} that, when resolved, returns the created resource.
47+
*/
48+
async createFor(tenant: TenantId, cancellation: Cancellation = Cancellation.default): Promise<TResource> {
49+
try {
50+
this._logger.debug(`Getting ${this._resource} resource for tenant ${tenant}`);
51+
52+
const executionContext = this._executionContext.forTenant(tenant);
53+
const callContext = ExecutionContexts.toCallContext(executionContext);
54+
55+
const request = this.createResourceRequest(callContext);
56+
const response = await reactiveUnary(this._client, this._method, request, cancellation).toPromise();
57+
58+
const [requestFailed, requestFailure] = this.requestFailed(response);
59+
if (requestFailed) {
60+
this._logger.warn(`Failed getting ${this._resource} resource for tenant ${tenant} because ${requestFailure!.getReason()}`);
61+
throw new FailedToGetResourceForTenant(this._resource, tenant, requestFailure!.getReason());
62+
}
63+
64+
return await this.createResourceFrom(response);
65+
} catch (error) {
66+
if (error instanceof FailedToGetResourceForTenant) {
67+
throw error;
68+
}
69+
70+
this._logger.warn(`Failed getting ${this._resource} resource for tenant ${tenant}}`, error);
71+
throw new FailedToGetResourceForTenant(this._resource, tenant, error as any);
72+
}
73+
}
74+
75+
/**
76+
* Creates a request to get the resource configuration using the provided call context.
77+
* @param {CallRequestContext} callContext - The call context to use for the request containing the tenant id.
78+
* @returns {TRequest} A new request.
79+
*/
80+
protected abstract createResourceRequest(callContext: CallRequestContext): TRequest;
81+
82+
/**
83+
* Checks whether the request failed based on the response.
84+
* @param {TResponse} response - The response received from the Runtime.
85+
* @returns {[false] | [true, Failure]} False if the request succeeded, true and the failure if it failed.
86+
*/
87+
protected abstract requestFailed(response: TResponse): [false] | [true, Failure];
88+
89+
/**
90+
* Creates the resource from the configuration received from the Runtime.
91+
* @param {TResponse} response - The response received from the Runtime.
92+
* @returns {Promise<TResource>} - A {@link Promise} that, when resolved, returns the created resource.
93+
*/
94+
protected abstract createResourceFrom(response: TResponse): Promise<TResource>;
95+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Dolittle. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { Logger } from 'winston';
5+
6+
import { ComplexValueMap } from '@dolittle/sdk.artifacts';
7+
import { ExecutionContext, TenantId } from '@dolittle/sdk.execution';
8+
import { Cancellation } from '@dolittle/sdk.resilience';
9+
import { Tenant } from '@dolittle/sdk.tenancy';
10+
11+
import { ResourcesClient } from '@dolittle/runtime.contracts/Resources/Resources_grpc_pb';
12+
13+
import { MongoDBResourceCreator } from '../MongoDB/Internal/MongoDBResourceCreator';
14+
import { IResources } from '../IResources';
15+
import { IResourcesBuilder } from '../IResourcesBuilder';
16+
import { Resources } from '../Resources';
17+
import { ResourcesBuilder } from '../ResourcesBuilder';
18+
import { IFetchResources } from './IFetchResources';
19+
20+
/**
21+
* Represents an implementation of {@link IFetchResources}.
22+
*/
23+
export class ResourcesFetcher extends IFetchResources {
24+
private readonly _mongoDB: MongoDBResourceCreator;
25+
26+
/**
27+
* Initialises a new instance of the {@link ResourcesFetcher} class.
28+
* @param {ResourcesClient} client - The resources client to make requests to the Runtime with.
29+
* @param {ExecutionContext} executionContext - The base execution context for the client.
30+
* @param {Logger} logger - The logger to use for logging.
31+
*/
32+
constructor(
33+
client: ResourcesClient,
34+
executionContext: ExecutionContext,
35+
logger: Logger,
36+
) {
37+
super();
38+
39+
this._mongoDB = new MongoDBResourceCreator(client, executionContext, logger);
40+
}
41+
42+
/** @inheritdoc */
43+
async fetchResourcesFor(tenants: readonly Tenant[], cancellation: Cancellation = Cancellation.default): Promise<IResourcesBuilder> {
44+
const resources: Map<TenantId, IResources> = new ComplexValueMap(TenantId, _ => [_.value.toString()], 1);
45+
46+
await Promise.all(tenants.map(async tenant => {
47+
resources.set(
48+
tenant.id,
49+
new Resources(
50+
await this._mongoDB.createFor(tenant.id, cancellation),
51+
),
52+
);
53+
}));
54+
55+
return new ResourcesBuilder(resources);
56+
}
57+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Dolittle. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
export { IFetchResources } from './IFetchResources';
5+
export { ResourceCreator } from './ResourceCreator';
6+
export { ResourcesFetcher } from './ResourcesFetcher';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Dolittle. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { Collection } from 'mongodb';
5+
import { Constructor } from '@dolittle/types';
6+
7+
import { Abstract, ServiceIdentifier } from '@dolittle/sdk.dependencyinversion';
8+
9+
declare module 'mongodb' {
10+
namespace Collection {
11+
/**
12+
* Gets a {@link ServiceIdentifier} for a read model type to inject a {@link Collection} from the service provider.
13+
* @param {Constructor} type - The type of the read model.
14+
* @returns {Abstract} The service identifier to use for injection.
15+
*/
16+
function forReadModel<TReadModel>(type: Constructor<TReadModel>): Abstract<Collection<TReadModel>>;
17+
}
18+
}
19+
20+
Collection.forReadModel = function forReadModel<TReadModel>(type: Constructor<TReadModel>): Abstract<Collection<TReadModel>> {
21+
return `Collection<${type.name}>` as any;
22+
};

0 commit comments

Comments
 (0)