Skip to content

Commit

Permalink
feat: added useModel to AbstractFirebaseNestContext
Browse files Browse the repository at this point in the history
- added useFirebaseModelsService and selectFromFirebaseModelService functions
- added UsePromiseFunction
- added read, update, and delete granted roles
- improved retrieving/using models in nest context
  • Loading branch information
dereekb committed Jun 4, 2022
1 parent 7786a40 commit 29c1940
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 61 deletions.
2 changes: 1 addition & 1 deletion apps/demo-api/src/app/function/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class DemoApiNestContext extends AbstractFirebaseNestContext<DemoFirebase
return this.nest.get(GuestbookServerActions);
}

get modelsService() {
get firebaseModelsService() {
return demoFirebaseModelServices;
}

Expand Down
22 changes: 20 additions & 2 deletions apps/demo-api/src/app/function/profile/profile.update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,26 @@ import { profileForUser } from './profile.util';
export const updateProfile: DemoUpdateModelfunction<UpdateProfileParams> = async (nest, data, context) => {
const updateProfile = await nest.profileActions.updateProfile(data);

const uid = updateProfile.params.uid ?? context.auth.uid;
const profileDocument: ProfileDocument = profileForUser(nest, uid);
const uid = context.auth.uid;
let profileDocument: ProfileDocument;

if (updateProfile.params.key != null) {
profileDocument = await nest.useModel('profile', {
context,
key: updateProfile.params.key,
roles: 'read',
use: (x) => x.document
});

// Alternative way using model() chain
/*
profileDocument = await nest.model(context)('profile')(updateProfile.params.key)('read')((x) => {
return x.document;
});
*/
} else {
profileDocument = profileForUser(nest, uid);
}

await updateProfile(profileDocument);
};
44 changes: 34 additions & 10 deletions packages/firebase-server/src/lib/nest/nest.provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FirebaseAppModelContext, FirebaseModelsService, InContextFirebaseModelsService, inContextFirebaseModelsServiceFactory } from '@dereekb/firebase';
import { FirebaseAppModelContext, FirebaseModelServiceContext, FirebaseModelsService, FirebaseModelsServiceSelectionResultRolesReader, FirebaseModelsServiceTypes, InContextFirebaseModelsService, inContextFirebaseModelsServiceFactory, UseFirebaseModelsServiceSelection, UseFirebaseModelsServiceSelectionUseFunction, useFirebaseModelsService } from '@dereekb/firebase';
import { build, BuildFunction, Getter } from '@dereekb/util';
import { INestApplicationContext } from '@nestjs/common';
import { AuthDataRef } from '../auth';
Expand Down Expand Up @@ -30,11 +30,11 @@ export abstract class AbstractNestContext {
constructor(readonly nest: INestApplicationContext) {}
}

export abstract class AbstractFirebaseNestContext<C, Y extends FirebaseModelsService<any, any>> extends AbstractNestContext {
export abstract class AbstractFirebaseNestContext<A, Y extends FirebaseModelsService<any, FirebaseAppModelContext<A>>> extends AbstractNestContext {
abstract get actionContext(): FirebaseServerActionsContext;
abstract get authService(): FirebaseServerAuthService;
abstract get modelsService(): Y;
abstract get app(): C;
abstract get firebaseModelsService(): Y;
abstract get app(): A;

/**
* Creates a FirebaseAppModelContext instance.
Expand All @@ -43,8 +43,8 @@ export abstract class AbstractFirebaseNestContext<C, Y extends FirebaseModelsSer
* @param buildFn
* @returns
*/
modelContext(auth: AuthDataRef, buildFn?: BuildFunction<FirebaseAppModelContext<C>>): FirebaseAppModelContext<C> {
const base: FirebaseAppModelContext<C> = {
makeModelContext(auth: AuthDataRef, buildFn?: BuildFunction<FirebaseAppModelContext<A>>): FirebaseAppModelContext<A> {
const base: FirebaseAppModelContext<A> = {
auth: this.authService.authContextInfo(auth),
app: this.app,
makePermissionError: nestFirebaseForbiddenPermissionError
Expand All @@ -61,12 +61,36 @@ export abstract class AbstractFirebaseNestContext<C, Y extends FirebaseModelsSer
/**
* Creates a InContextFirebaseModelsService given the input context and parameters.
*
* @param auth
* @param context
* @param buildFn
* @returns
*/
service(auth: AuthDataRef, buildFn?: BuildFunction<FirebaseAppModelContext<C>>): InContextFirebaseModelsService<Y> {
const firebaseModelContext = this.modelContext(auth, buildFn);
return inContextFirebaseModelsServiceFactory(this.modelsService)(firebaseModelContext) as InContextFirebaseModelsService<Y>;
model(context: AuthDataRef, buildFn?: BuildFunction<FirebaseAppModelContext<A>>): InContextFirebaseModelsService<Y> {
const firebaseModelContext = this.makeModelContext(context, buildFn);
return inContextFirebaseModelsServiceFactory(this.firebaseModelsService)(firebaseModelContext) as InContextFirebaseModelsService<Y>;
}

async useModel<T extends FirebaseModelsServiceTypes<Y>, O>(type: T, select: UseModelInput<FirebaseAppModelContext<A>, Y, T, O>): Promise<O>;
async useModel<T extends FirebaseModelsServiceTypes<Y>>(type: T, select: UseModelInputForRolesReader<FirebaseAppModelContext<A>, Y, T>): Promise<FirebaseModelsServiceSelectionResultRolesReader<Y, T>>;
async useModel<T extends FirebaseModelsServiceTypes<Y>, O>(type: T, select: UseModelInput<FirebaseAppModelContext<A>, Y, T, O> | UseModelInputForRolesReader<FirebaseAppModelContext<A>, Y, T>): Promise<any> {
const appModelContext: FirebaseAppModelContext<A> = this.makeModelContext(select.context, select.buildFn);
const usePromise = useFirebaseModelsService(this.firebaseModelsService, type, {
context: appModelContext,
key: select.key,
roles: select.roles,
rolesSetIncludes: select.rolesSetIncludes
} as UseFirebaseModelsServiceSelection<Y, T>);

const use: UseFirebaseModelsServiceSelectionUseFunction<Y, T, O> = (select as UseModelInput<FirebaseAppModelContext<A>, Y, T, O>).use ?? ((x) => x as unknown as O);
return usePromise(use);
}
}

export type UseModelInputForRolesReader<C extends FirebaseModelServiceContext, Y extends FirebaseModelsService<any, C>, T extends FirebaseModelsServiceTypes<Y>> = Omit<UseFirebaseModelsServiceSelection<Y, T>, 'type' | 'context'> & {
context: AuthDataRef;
buildFn?: BuildFunction<C>;
};

export type UseModelInput<C extends FirebaseModelServiceContext, Y extends FirebaseModelsService<any, C>, T extends FirebaseModelsServiceTypes<Y>, O> = UseModelInputForRolesReader<C, Y, T> & {
use: UseFirebaseModelsServiceSelectionUseFunction<Y, T, O>;
};
129 changes: 126 additions & 3 deletions packages/firebase/src/lib/common/model/model.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { MockFirebaseContext, authorizedFirestoreFactory, MockItemCollectionFixture, testWithMockItemFixture, MOCK_FIREBASE_MODEL_SERVICE_FACTORIES, mockFirebaseModelServices, MockItem, MockItemDocument, MockItemRoles } from '@dereekb/firebase/test';
import { GrantedRoleMap, isFullAccessRoleMap, isNoAccessRoleMap } from '@dereekb/model';
import { Building } from '@dereekb/util';
import { ArrayOrValue, Building, UsePromiseFunction } from '@dereekb/util';
import { makeDocuments } from '../firestore';
import { FirestoreDocumentAccessor } from '../firestore/accessor/document';
import { firebaseModelsService, inContextFirebaseModelsServiceFactory, InModelContextFirebaseModelServiceFactory } from './model.service';
import { firebaseModelsService, inContextFirebaseModelsServiceFactory, InModelContextFirebaseModelServiceFactory, selectFromFirebaseModelsService, useFirebaseModelsService } from './model.service';
import { ContextGrantedModelRolesReader } from './permission/permission.service.role';

describe('firebaseModelsService', () => {
describe('with mockFirebaseModelServices', () => {
Expand Down Expand Up @@ -39,6 +40,128 @@ describe('firebaseModelsService', () => {
item = items[0];
});

describe('selection', () => {
describe('selectFromFirebaseModelsService()', () => {
it('should return an InModelContextFirebaseModelService instance with the specified model.', () => {
const result = selectFromFirebaseModelsService(mockFirebaseModelServices, 'mockItem', {
context,
key: item.key
});

expect(typeof result).toBe('function');
expect(result).toBeDefined();
expect(result.requireRole).toBeDefined();
expect(result.requireUse).toBeDefined();
expect(result.use).toBeDefined();
expect(result.roleMap).toBeDefined();
expect(result.roleReader).toBeDefined();
expect(result.model).toBeDefined();
expect(result.model.key).toBe(item.key);
});
});

describe('useFirebaseModelsService()', () => {
it('should create a function that uses the target model.', () => {
const useFn = useFirebaseModelsService(mockFirebaseModelServices, 'mockItem', {
context,
key: item.key
});

expect(useFn).toBeDefined();
expect(typeof useFn).toBe('function');
});

describe('function', () => {
let useFn: UsePromiseFunction<ContextGrantedModelRolesReader<MockFirebaseContext, MockItem, MockItemDocument, MockItemRoles>>;

function setUseFnWithContext(partialContext: Partial<MockFirebaseContext>, roles: ArrayOrValue<MockItemRoles> = 'read') {
useFn = useFirebaseModelsService(mockFirebaseModelServices, 'mockItem', {
context: {
...context,
...partialContext
},
key: item.key,
roles
});
}

beforeEach(() => {
useFn = useFirebaseModelsService(mockFirebaseModelServices, 'mockItem', {
context,
key: item.key
});
});

it('should use the model.', async () => {
let used = false;

const value = 0;
const result = await useFn((x) => {
expect(x.data).toBeDefined();
expect(x.document).toBeDefined();
expect(x.snapshot).toBeDefined();

used = true;

return value;
});

expect(used).toBe(true);
expect(result).toBe(value);
});

describe('with roles', () => {
const readRoleKey = 'read';

it('should use the model if the context is granted the expected roles.', async () => {
setUseFnWithContext({
rolesToReturn: {
[readRoleKey]: true
}
});

let used = false;

const value = 0;
const result = await useFn((x) => {
expect(x.hasRole(readRoleKey)).toBe(true);
used = true;
return value;
});

expect(used).toBe(true);
expect(result).toBe(value);
});

it('should throw an exception if the model is not granted the expected roles.', async () => {
setUseFnWithContext({
rolesToReturn: {
[readRoleKey]: false // not allowed to read
}
});

let used = false;

const value = 0;

try {
await useFn((x) => {
expect(x.hasRole(readRoleKey)).toBe(true);
used = true;
return value;
});
fail(new Error('should have not been used.'));
} catch (e) {
expect(e).toBeDefined();
}

expect(used).toBe(false);
});
});
});
});
});

describe('inContextFirebaseModelsServiceFactory', () => {
it('should create an InContextFirebaseModelsServiceFactory', () => {
const x = inContextFirebaseModelsServiceFactory(mockFirebaseModelServices);
Expand All @@ -53,7 +176,7 @@ describe('firebaseModelsService', () => {
const inModelContextFactory = inContext(item);

expect(inModelContextFactory).toBeDefined();
expect(typeof inModelContextFactory === 'object').toBe(true);
expect(typeof inModelContextFactory).toBe('function');
});

describe('service', () => {
Expand Down
Loading

0 comments on commit 29c1940

Please sign in to comment.