Skip to content

Commit cef950d

Browse files
committed
(WiP): fixing multi tenancy
1 parent c13b2b5 commit cef950d

23 files changed

+253
-285
lines changed

Diff for: LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License
2+
3+
Copyright (c) 2010-2019 Google, Inc. http://angularjs.org
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

Diff for: README.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ ULTIMATE BACKEND
88
<p align="center">
99
</p>
1010

11+
<a href="https://img.shields.io/github/license/juicycleff/ultimate-backend?style=for-the-badge" target="_blank"><img src="https://img.shields.io/github/license/juicycleff/ultimate-backend?style=for-the-badge" alt="License"/></a>
12+
<a href="https://img.shields.io/snyk/vulnerabilities/github/juicycleff/ultimate-backend?style=for-the-badge" target="_blank"><img src="https://img.shields.io/snyk/vulnerabilities/github/juicycleff/ultimate-backend?style=for-the-badge" alt="Snyk"/></a>
13+
1114
## Description
1215
This should be the go to backend base for your next scalable project. It comes with Multi-Tenancy, following different multi-tenancy database strategy as well as different resolver patterns
1316
to identify your tenants. The goal is to give your next big project that extra leap to awesomeness.

Diff for: apps/gateway-admin/src/app.module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Module } from '@nestjs/common';
1+
import { CacheModule, Module } from '@nestjs/common';
22
import { AppController } from './app.controller';
33
import { AppService } from './app.service';
44
import { GraphqlDistributedGatewayModule } from '@graphqlcqrs/graphql-gateway';
55
import { HeadersDatasource } from './headers.datasource';
66

77
@Module({
88
imports: [
9+
CacheModule.register(),
910
GraphqlDistributedGatewayModule.forRoot({
1011
subscriptions: false,
1112
path: '/graphql',

Diff for: apps/gateway-admin/src/headers.datasource.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class HeadersDatasource extends RemoteGraphQLDataSource {
55
willSendRequest({ request, context }) {
66

77
if (context.req) {
8-
if (context.req && context.req.headers) {
8+
if (context.req.headers) {
99
const ctxHeaders = context.req.headers;
1010

1111
for (const key in ctxHeaders) {
@@ -14,10 +14,16 @@ export class HeadersDatasource extends RemoteGraphQLDataSource {
1414
}
1515
}
1616
}
17-
if (context.req && context.req.cookies) {
17+
if (context.req.cookies) {
1818
// tslint:disable-next-line:no-console
1919
console.log('cookies', context.req.cookies);
2020
}
21+
if (context.req.tenantInfo) {
22+
// tslint:disable-next-line:no-console
23+
console.log('tenantInfo start', context.req.tenantInfo);
24+
request.tenantInfo = context.req.tenantInfo;
25+
request.http.headers.set('x-tenant-info', JSON.stringify(context.req.tenantInfo));
26+
}
2127
}
2228
}
2329

Diff for: apps/gateway-admin/src/main.ts

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ async function bootstrap() {
2121
}));
2222
AppUtils.killAppWithGrace(app);
2323
app.use(cookieParser());
24-
2524
await app.listen(parseInt(process.env.PORT, 10) || 4000);
2625
}
2726
bootstrap();

Diff for: apps/service-auth/src/auth/auth.resolver.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
22
import { UseGuards } from '@nestjs/common';
33
import { GqlAuthGuard } from '@graphqlcqrs/common/guards';
44
import { AuthService } from './auth.service';
5+
import { AuthEntity } from '@graphqlcqrs/repository/entities';
56

67
@Resolver('AuthPayload')
78
export class AuthResolver {
@@ -13,10 +14,15 @@ export class AuthResolver {
1314
async login(@Args('input') {identifier, password}: any, @Context() context: any) {
1415
console.log('************************');
1516
console.log(context.req.session);
16-
const gt = await this.authService.create({
17-
name: 'catty',
18-
sound: 'meow',
19-
});
17+
let auth = new AuthEntity();
18+
// @ts-ignore
19+
auth = {
20+
local: {
21+
22+
hashedPassword: 'blaaaaab',
23+
},
24+
};
25+
const gt = await this.authService.create(auth);
2026
// console.log(gt);
2127
console.log('************************');
2228
const { user } = await context.authenticate('graphql-local', { email: identifier, password });

Diff for: apps/service-auth/src/auth/auth.service.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { Injectable } from '@nestjs/common';
22
import { AuthRepository } from '@graphqlcqrs/repository/repositories/auth.repository';
3+
import { AuthEntity } from '@graphqlcqrs/repository/entities';
34

45
@Injectable()
56
export class AuthService {
67
constructor(private authRepository: AuthRepository) {}
78

8-
async create(cat: any) {
9-
// @ts-ignore
10-
return await this.authRepository.create({
11-
local: {
12-
13-
hashedPassword: 'green',
14-
},
15-
}); // save(cat);
9+
async create(auth: Partial<AuthEntity> | AuthEntity) {
10+
try {
11+
return await this.authRepository.create(auth);
12+
} catch (e) {
13+
console.log(e);
14+
}
1615
}
1716

1817
async validateUser(email: string, pass: string): Promise<any> {

Diff for: libs/common/src/utils/cache.utils.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Validator } from 'class-validator';
2+
import * as stringify from 'json-stable-stringify';
3+
import { hash } from './hash.utils';
4+
5+
/**
6+
* @description: Utility functions to support caching operations
7+
* @notes:
8+
* - Uses json-stable-stringify (instead of JSON.Stringify) for determanistic string generation - regardless of parameter ordering
9+
* - Uses custom hash function as significantly faster than cryptogaphic hashes
10+
*/
11+
export class CachingUtils {
12+
private static validator = new Validator();
13+
14+
public static makeCacheKeyFromId(entityId: string): string {
15+
this.validator.isMongoId(entityId);
16+
return this.makeCacheKeyFromProperty(entityId, 'id');
17+
}
18+
19+
public static makeCacheKeyFromProperty(
20+
propertyName: string,
21+
propertyValue: string,
22+
): string {
23+
this.validator.isNotEmpty(propertyValue);
24+
this.validator.isNotEmpty(propertyName);
25+
return `CacheKey-${propertyName}-${propertyValue}`;
26+
}
27+
28+
public static makeCacheKeyFromObject(object: object): string {
29+
return hash(stringify(object)).toString();
30+
}
31+
}

Diff for: libs/nest-multi-tenant/src/database/mongo/decorators/entity-repository.decorator.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { COLLECTION_KEY, CollectionProps } from '../interfaces';
1+
import { COLLECTION_KEY, CollectionProps, ENTITY_KEY, EntityProps } from '../interfaces';
22

33
/**
44
* Indicate the class represents a collection
@@ -10,3 +10,14 @@ import { COLLECTION_KEY, CollectionProps } from '../interfaces';
1010
export const EntityRepository = (props: CollectionProps) => (target: any) => {
1111
Reflect.defineMetadata(COLLECTION_KEY, props, target.prototype);
1212
};
13+
14+
/**
15+
* Indicate the class represents a collection
16+
*
17+
* @export
18+
* @param {CollectionProps} props
19+
* @returns
20+
*/
21+
export const Entity = (props?: EntityProps) => (target: any) => {
22+
Reflect.defineMetadata(ENTITY_KEY, props, target.prototype);
23+
};

Diff for: libs/nest-multi-tenant/src/database/mongo/decorators/hooks.decorator.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { POST_KEY, PRE_KEY } from '../interfaces';
2+
import { DataEvents } from '@juicycleff/nest-multi-tenant/enums';
23

34
/**
45
* Run this function before an event occurs
@@ -17,7 +18,7 @@ import { POST_KEY, PRE_KEY } from '../interfaces';
1718
* @param {...string[]} events a list of events
1819
* @returns
1920
*/
20-
export const Before = (...events: string[]) => (target: any, name: string, descriptor: TypedPropertyDescriptor<any>) => {
21+
export const Before = (...events: DataEvents[]) => (target: any, name: string, descriptor: TypedPropertyDescriptor<any>) => {
2122
for (const event of events) {
2223
const fns = Reflect.getMetadata(`${PRE_KEY}_${event}`, target) || [];
2324
// you must create new array so you don't push fn into siblings
@@ -44,7 +45,7 @@ export const Before = (...events: string[]) => (target: any, name: string, descr
4445
* @param {...string[]} events a list of events
4546
* @returns
4647
*/
47-
export const After = (...events: string[]) => (target: any, name: string, descriptor: TypedPropertyDescriptor<any>) => {
48+
export const After = (...events: DataEvents[]) => (target: any, name: string, descriptor: TypedPropertyDescriptor<any>) => {
4849
for (const event of events) {
4950
const fns = Reflect.getMetadata(`${POST_KEY}_${event}`, target) || [];
5051
// you must create new array so you don't push fn into siblings
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './mongo.decorator';
22
export * from './entity-repository.decorator';
33
export * from './hooks.decorator';
4-
export * from './entity.decorator';
4+
// export * from './entity.decorator';
55
export * from './object-id-column.decorator';

Diff for: libs/nest-multi-tenant/src/database/mongo/interfaces/types.interface.ts

+6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export interface CollectionProps {
3232
indexes?: IndexDefinition[];
3333
}
3434

35+
export interface EntityProps {
36+
name?: string;
37+
supportTenant?: boolean;
38+
indexes?: IndexDefinition[];
39+
}
40+
3541
export interface IndexDefinition {
3642
// The fields to index on
3743
fields: { [fieldName: string]: string | any };

Diff for: libs/nest-multi-tenant/src/database/mongo/repository/base.repository.ts

+38-23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { Collection, ObjectID, DeleteWriteOpResultObject } from 'mongodb';
2-
import {
3-
COLLECTION_KEY, CollectionProps, DBSource, FindRequest,
4-
POST_KEY, PRE_KEY, UpdateByIdRequest, UpdateRequest,
5-
} from '../interfaces';
1+
import { Collection, DeleteWriteOpResultObject, ObjectID } from 'mongodb';
2+
import { COLLECTION_KEY, CollectionProps, DBSource, FindRequest, POST_KEY, PRE_KEY, UpdateByIdRequest, UpdateRequest } from '../interfaces';
3+
import { DataEvents } from '@juicycleff/nest-multi-tenant/enums';
64

75
// that class only can be extended
86
export class BaseRepository <DOC, DTO = DOC> {
@@ -51,7 +49,7 @@ export class BaseRepository <DOC, DTO = DOC> {
5149

5250
const results: DOC[] = [];
5351
for (const result of found) {
54-
results.push(await this.invokeEvents(POST_KEY, ['find', 'findMany'], this.toggleId(result, false)));
52+
results.push(await this.invokeEvents(POST_KEY, ['FIND', 'FIND_MANY'], this.toggleId(result, false)));
5553
}
5654

5755
return results;
@@ -70,7 +68,7 @@ export class BaseRepository <DOC, DTO = DOC> {
7068
let document = await collection.findOne(conditions);
7169
if (document) {
7270
document = this.toggleId(document, false);
73-
document = await this.invokeEvents(POST_KEY, ['find', 'findOne'], document);
71+
document = await this.invokeEvents(POST_KEY, ['FIND', 'FIND_ONE'], document);
7472
return document;
7573
}
7674
}
@@ -109,7 +107,7 @@ export class BaseRepository <DOC, DTO = DOC> {
109107

110108
for (let document of newDocuments) {
111109
document = this.toggleId(document, false);
112-
document = await this.invokeEvents(POST_KEY, ['find', 'findMany'], document);
110+
document = await this.invokeEvents(POST_KEY, ['FIND', 'FIND_MANY'], document);
113111
results.push(document);
114112
}
115113

@@ -123,15 +121,18 @@ export class BaseRepository <DOC, DTO = DOC> {
123121
* @returns {Promise<DOC>}
124122
* @memberof BaseRepository
125123
*/
126-
async create(document: DTO): Promise<DOC> {
124+
async create(document: Partial<DTO> | DTO): Promise<DOC> {
127125
const collection = await this.collection;
128-
const eventResult: unknown = await this.invokeEvents(PRE_KEY, ['save', 'create'], document);
126+
const eventResult: unknown = await this.invokeEvents(PRE_KEY, ['SAVE', 'CREATE'], document);
127+
128+
console.log(eventResult);
129+
129130
const res = await collection.insertOne(eventResult as DOC);
130131

131132
let newDocument = res.ops[0];
132133
// @ts-ignore
133134
newDocument = this.toggleId(newDocument, false);
134-
newDocument = await this.invokeEvents(POST_KEY, ['save', 'create'], newDocument);
135+
newDocument = await this.invokeEvents(POST_KEY, ['SAVE', 'CREATE'], newDocument);
135136
// @ts-ignore
136137
return newDocument;
137138
}
@@ -149,7 +150,7 @@ export class BaseRepository <DOC, DTO = DOC> {
149150
// @ts-ignore
150151
const id = new ObjectID(document.id); // flip/flop ids
151152

152-
const updates = await this.invokeEvents(PRE_KEY, ['save'], document);
153+
const updates = await this.invokeEvents(PRE_KEY, ['SAVE'], document);
153154
delete updates.id;
154155
delete updates._id;
155156
const query = { _id: id };
@@ -166,7 +167,7 @@ export class BaseRepository <DOC, DTO = DOC> {
166167
// @ts-ignore
167168
delete newDocument._id;
168169

169-
newDocument = await this.invokeEvents(POST_KEY, ['save'], newDocument);
170+
newDocument = await this.invokeEvents(POST_KEY, ['SAVE'], newDocument);
170171
return newDocument;
171172
}
172173

@@ -195,7 +196,7 @@ export class BaseRepository <DOC, DTO = DOC> {
195196
*/
196197
async findOneAndUpdate(req: UpdateRequest): Promise<DOC> {
197198
const collection = await this.collection;
198-
const updates = await this.invokeEvents(PRE_KEY, ['update', 'updateOne'], req.updates);
199+
const updates = await this.invokeEvents(PRE_KEY, ['UPDATE', 'UPDATE_ONE'], req.updates);
199200

200201
const res = await collection.findOneAndUpdate(req.conditions, updates, {
201202
upsert: req.upsert,
@@ -204,7 +205,7 @@ export class BaseRepository <DOC, DTO = DOC> {
204205

205206
let document = res.value;
206207
document = this.toggleId(document, false);
207-
document = await this.invokeEvents(POST_KEY, ['update', 'updateOne'], document);
208+
document = await this.invokeEvents(POST_KEY, ['UPDATE', 'UPDATE_ONE'], document);
208209
return document;
209210
}
210211

@@ -229,9 +230,9 @@ export class BaseRepository <DOC, DTO = DOC> {
229230
async deleteOne(conditions: any): Promise<DeleteWriteOpResultObject> {
230231
const collection = await this.collection;
231232

232-
await this.invokeEvents(PRE_KEY, ['delete', 'deleteOne'], conditions);
233-
const deleteResult = collection.deleteOne(conditions);
234-
await this.invokeEvents(POST_KEY, ['delete', 'deleteOne'], deleteResult);
233+
await this.invokeEvents(PRE_KEY, ['DELETE', 'DELETE_ONE'], conditions);
234+
const deleteResult = await collection.deleteOne(conditions);
235+
await this.invokeEvents(POST_KEY, ['DELETE', 'DELETE_ONE'], deleteResult);
235236

236237
return deleteResult;
237238
}
@@ -246,13 +247,25 @@ export class BaseRepository <DOC, DTO = DOC> {
246247
async deleteMany(conditions: any): Promise<DeleteWriteOpResultObject> {
247248
const collection = await this.collection;
248249

249-
await this.invokeEvents(PRE_KEY, ['delete', 'deleteMany'], conditions);
250-
const deleteResult = collection.deleteMany(conditions);
251-
await this.invokeEvents(POST_KEY, ['delete', 'deleteMany'], deleteResult);
250+
await this.invokeEvents(PRE_KEY, ['DELETE_ONE', 'DELETE_MANY'], conditions);
251+
const deleteResult = await collection.deleteMany(conditions);
252+
await this.invokeEvents(POST_KEY, ['DELETE_ONE', 'DELETE_MANY'], deleteResult);
252253

253254
return deleteResult;
254255
}
255256

257+
/**
258+
* Delete multiple records
259+
*
260+
* @param {*} conditions
261+
* @returns {Promise<any>}
262+
* @memberof BaseRepository
263+
*/
264+
async exists(conditions: any): Promise<boolean> {
265+
const collection = await this.collection;
266+
return await collection.find(conditions).count() > 0;
267+
}
268+
256269
/**
257270
* Strip off Mongo's ObjectID and replace with string representation or in reverse
258271
*
@@ -341,12 +354,14 @@ export class BaseRepository <DOC, DTO = DOC> {
341354
* @returns {Promise<DOC>}
342355
* @memberof BaseRepository
343356
*/
344-
private async invokeEvents(type: string, fns: string[], document: any): Promise<any> {
357+
private async invokeEvents(type: string, fns: DataEvents[], document: any): Promise<any> {
358+
const test = Reflect.getMetadata('entity', this) || [];
359+
console.log(document, test);
345360
for (const fn of fns) {
346361
const events = Reflect.getMetadata(`${type}_${fn}`, this) || [];
347362
for (const event of events) {
348363
document = event.bind(this)(document);
349-
if (typeof document.then === 'function') {
364+
if (document !== undefined && typeof document.then === 'function') {
350365
document = await document;
351366
}
352367
}

0 commit comments

Comments
 (0)