Skip to content

Commit

Permalink
feat: added firebaseModelsService
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Jun 1, 2022
1 parent 3876575 commit 7432e55
Show file tree
Hide file tree
Showing 18 changed files with 394 additions and 57 deletions.
13 changes: 13 additions & 0 deletions components/demo-firebase/src/lib/models/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FirebaseAppModelContext } from '@dereekb/firebase';
import { GuestbookTypes, GuestbookFirestoreCollections } from './guestbook';
import { ProfileTypes, ProfileFirebaseContext } from './profile';

// todo...

export type GuestbookFirebaseContext = FirebaseAppModelContext<GuestbookFirestoreCollections>;

export type GuestbookModelServiceFactories = {};

export type DemoFirebaseModelTypes = GuestbookTypes | ProfileTypes;

export type DemoFirebaseContext = GuestbookFirebaseContext | ProfileFirebaseContext;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Guestbook, GuestbookDocument, GuestbookEntry, GuestbookEntryDocument, GuestbookEntryRoles, GuestbookFirestoreCollections, GuestbookRoles } from './guestbook';
import { FirebaseAppModelContext, FirebasePermissionServiceModel, FirebaseModelServiceConfig, firebaseModelServiceFactory } from '@dereekb/firebase';
import { FirebaseAppModelContext, FirebasePermissionServiceModel, firebaseModelServiceFactory } from '@dereekb/firebase';
import { GrantedRoleMap, noAccessRolesMap } from '@dereekb/model';
import { PromiseOrValue } from '@dereekb/util';

Expand Down
15 changes: 7 additions & 8 deletions components/demo-firebase/src/lib/service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { DemoFirestoreCollections } from './collection';
import { GuestbookTypes, profileFirebaseModelServiceFactory, profilePrivateDataFirebaseModelServiceFactory, guestbookFirebaseModelServiceFactory, guestbookEntryFirebaseModelServiceFactory, ProfileTypes } from './models';
import { firebaseModelsService } from '@dereekb/firebase';
import { grantedRoleMapReader } from '@dereekb/model';
import { GuestbookTypes, profileFirebaseModelServiceFactory, profilePrivateDataFirebaseModelServiceFactory, guestbookFirebaseModelServiceFactory, guestbookEntryFirebaseModelServiceFactory, ProfileTypes, GuestbookFirebaseContext, ProfileFirebaseContext } from './models';

export type DemoFirebaseModelTypes = GuestbookTypes | ProfileTypes;

export interface DemoFirebaseModelServiceConfig {
readonly demoFirestoreCollections: DemoFirestoreCollections;
}
export type DemoFirebaseContext = GuestbookFirebaseContext | ProfileFirebaseContext;

export type DemoFirebaseContext = {};

export const DEMO_FIREBASE_MODEL_SERVICES = {
export const DEMO_FIREBASE_MODEL_SERVICE_FACTORIES = {
guestbook: guestbookFirebaseModelServiceFactory,
guestbookentry: guestbookEntryFirebaseModelServiceFactory,
profile: profileFirebaseModelServiceFactory,
profileprivate: profilePrivateDataFirebaseModelServiceFactory
};

export const demoFirebaseModelServices = firebaseModelsService<typeof DEMO_FIREBASE_MODEL_SERVICE_FACTORIES, DemoFirebaseContext, DemoFirebaseModelTypes>(DEMO_FIREBASE_MODEL_SERVICE_FACTORIES);
12 changes: 11 additions & 1 deletion packages/firebase/src/lib/common/model/model/model.loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FirebaseTransactionContext, FirestoreCollectionLike, FirestoreDocument, LimitedFirestoreDocumentAccessor } from '../../firestore';
import { ModelLoader } from '@dereekb/model';
import { InContextModelLoader, ModelLoader } from '@dereekb/model';
import { ModelKey } from '@dereekb/util';

export type FirebaseModelLoaderContext = FirebaseTransactionContext;
Expand Down Expand Up @@ -39,3 +39,13 @@ export function firebaseModelLoader<C extends FirebaseModelLoaderContext, T, D e
}
};
}

// MARK: In Context
export interface InContextFirebaseModelLoader<T, D extends FirestoreDocument<T>> extends InContextModelLoader<D> {
loadModelForKey(key: ModelKey): D;
}

/**
* Type used to convert a FirebaseModelLoader into an InContextFirebaseModelLoader
*/
export type AsInContextFirebaseModelLoader<X> = X extends FirebaseModelLoader<infer C, infer T, infer D> ? InContextFirebaseModelLoader<T, D> : never;
127 changes: 127 additions & 0 deletions packages/firebase/src/lib/common/model/object/model.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { MockFirebaseContext, authorizedFirestoreFactory, MockItemCollectionFixture, testWithMockItemFixture, MOCK_FIREBASE_MODEL_SERVICE_FACTORIES, mockFirebaseModelServices, MockItem, MockItemDocument, MockItemRoles } from '@dereekb/firebase/test';
import { GrantedRoleMap, isNoAccessRolesMap } from '@dereekb/model';
import { makeDocuments } from '../../firestore';
import { FirestoreDocumentAccessor } from '../../firestore/accessor/document';
import { firebaseModelsService } from './model.service';

describe('firebaseModelsService', () => {
describe('with mockFirebaseModelServices', () => {
it('should create a FirebaseModelsService', () => {
const result = firebaseModelsService(MOCK_FIREBASE_MODEL_SERVICE_FACTORIES);

expect(result).toBeDefined();
expect(result.service).toBeDefined();
});
});

testWithMockItemFixture()(authorizedFirestoreFactory)((f: MockItemCollectionFixture) => {
describe('service', () => {
it('should create an InContextFirebaseModelService', async () => {
const context: MockFirebaseContext = {
app: f.instance.collections
};

const result = mockFirebaseModelServices.service('mockitem', context);
expect(result).toBeDefined();
expect(result.rolesMapForKey).toBeDefined();
expect(result.rolesMapForModel).toBeDefined();
expect(result.loadModelForKey).toBeDefined();
});

describe('InContextFirebaseModelPermissionService', () => {
let context: MockFirebaseContext;
let firestoreDocumentAccessor: FirestoreDocumentAccessor<MockItem, MockItemDocument>;
let item: MockItemDocument;

beforeEach(async () => {
context = {
app: f.instance.collections
};

firestoreDocumentAccessor = f.instance.firestoreCollection.documentAccessor();
const items = await makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
count: 1,
init: (i) => {
return {
value: `${i}`,
test: true,
string: ''
};
}
});

item = items[0];
});

describe('loadModelForKey()', () => {
it('should return a document for the input key', async () => {
const result = await mockFirebaseModelServices.service('mockitem', context).loadModelForKey(item.documentRef.path);
expect(result).toBeDefined();
expect(result.documentRef.path).toBe(item.documentRef.path);
});
});

describe('rolesMapForKey()', () => {
it('should return roles if the model exists.', async () => {
let testRoles: GrantedRoleMap<MockItemRoles> = {
read: true
};

context.rolesToReturn = testRoles; // configured to be returned

const result = await mockFirebaseModelServices.service('mockitem', context).rolesMapForKey(item.documentRef.path);
expect(result).toBeDefined();
expect(result.context).toBe(context);
expect(result.data).toBeDefined();
expect(result.data?.snapshot).toBeDefined();
expect(result.data?.document).toBeDefined();
expect(result.data?.exists).toBe(true);
expect(result.data?.data).toBeDefined();
expect(result.roles).toBe(testRoles);
});

it('should return empty roles if the model does not exist.', async () => {
await item.accessor.delete();

const result = await mockFirebaseModelServices.service('mockitem', context).rolesMapForKey(item.documentRef.path);
expect(result).toBeDefined();
expect(result.context).toBe(context);
expect(result.data).toBeDefined();
expect(result.data?.exists).toBe(false);
expect(result.data?.data).not.toBeDefined();
expect(result.roles).toBeDefined();
expect(isNoAccessRolesMap(result.roles)).toBe(true);
});
});

describe('rolesMapForModel()', () => {
it('should return roles if the model exists.', async () => {
let testRoles: GrantedRoleMap<MockItemRoles> = {
read: true
};

context.rolesToReturn = testRoles; // configured to be returned

const result = await mockFirebaseModelServices.service('mockitem', context).rolesMapForModel(item);
expect(result).toBeDefined();
expect(result.context).toBe(context);
expect(result.data).toBeDefined();
expect(result.roles).toBe(testRoles);
});

it('should return empty roles if the model does not exist.', async () => {
await item.accessor.delete();

const result = await mockFirebaseModelServices.service('mockitem', context).rolesMapForModel(item);
expect(result).toBeDefined();
expect(result.context).toBe(context);
expect(result.data).toBeDefined();
expect(result.data!.data).not.toBeDefined();
expect(result.roles).toBeDefined();
expect(isNoAccessRolesMap(result.roles)).toBe(true);
});
});
});
});
});
});
43 changes: 40 additions & 3 deletions packages/firebase/src/lib/common/model/object/model.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Getter, cachedGetter, build } from '@dereekb/util';
import { FirestoreDocument } from '../../firestore/accessor/document';
import { FirebaseModelCollectionLoader, firebaseModelLoader, FirebaseModelLoader } from '../model/model.loader';
import { FirebasePermissionContext } from '../permission/permission.context';
import { firebaseModelPermissionService, FirebaseModelPermissionService, FirebasePermissionServiceInstanceDelegate } from '../permission/permission.service';
import { FirebaseModelCollectionLoader, firebaseModelLoader, FirebaseModelLoader, InContextFirebaseModelLoader } from '../model/model.loader';
import { InContextFirebaseModelPermissionService, FirebasePermissionContext, firebaseModelPermissionService, FirebaseModelPermissionService, FirebasePermissionServiceInstanceDelegate } from '../permission';

export type FirebaseModelServiceContext = FirebasePermissionContext;

export interface FirebaseModelService<C extends FirebaseModelServiceContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> extends FirebaseModelPermissionService<C, T, D, R>, FirebaseModelLoader<C, T, D> {}
export type FirebaseModelServiceGetter<C extends FirebaseModelServiceContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> = Getter<FirebaseModelService<C, T, D, R>>;

export interface FirebaseModelServiceConfig<C extends FirebaseModelServiceContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> extends Omit<FirebasePermissionServiceInstanceDelegate<C, T, D, R>, 'loadModelForKey'>, FirebaseModelCollectionLoader<C, T, D> {}

Expand Down Expand Up @@ -34,3 +34,40 @@ export type FirebaseModelServiceFactory<C extends FirebaseModelServiceContext, T
export function firebaseModelServiceFactory<C extends FirebaseModelServiceContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string>(config: FirebaseModelServiceConfig<C, T, D, R>): FirebaseModelServiceFactory<C, T, D, R> {
return cachedGetter(() => firebaseModelService(config));
}

// MARK: InContext
export interface InContextFirebaseModelService<C, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> extends InContextFirebaseModelPermissionService<C, T, D, R>, InContextFirebaseModelLoader<T, D> {}
export type InContextFirebaseModelServiceFactory<C, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> = (context: C) => InContextFirebaseModelService<C, T, D, R>;

export function inContextFirebaseModelService<C, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string>(factory: FirebaseModelServiceGetter<C, T, D, R>): InContextFirebaseModelServiceFactory<C, T, D, R> {
return (context: C) => {
const firebaseModelService = factory();

const service: InContextFirebaseModelService<C, T, D, R> = {
rolesMapForModel: (model) => firebaseModelService.rolesMapForModelContext(model, context),
rolesMapForKey: (key) => firebaseModelService.rolesMapForKeyContext(key, context),
loadModelForKey: (key) => firebaseModelService.loadModelForKey(key, context)
};

return service;
};
}

// MARK: Service
export type FirebaseModelsServiceFactory<C extends FirebaseModelServiceContext, T extends string = string> = {
[K in T]: FirebaseModelServiceGetter<C, any>;
};

export type FirebaseModelsService<X extends FirebaseModelsServiceFactory<C>, C extends FirebaseModelServiceContext> = {
service<K extends keyof X>(type: K, context: C): X[K] extends FirebaseModelServiceGetter<C, infer T, infer D, infer R> ? InContextFirebaseModelService<C, T, D, R> : never;
};

export function firebaseModelsService<X extends FirebaseModelsServiceFactory<C, T>, C extends FirebaseModelServiceContext, T extends string = string>(services: X): FirebaseModelsService<X, C> {
return {
service: <K extends keyof X>(type: K, context: C) => {
const firebaseModelService = services[type] as FirebaseModelServiceGetter<C, unknown>;
const service = inContextFirebaseModelService(firebaseModelService)(context);
return service as any;
}
};
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { DocumentSnapshot, FirestoreDocument } from './../../firestore';
import { AbstractModelPermissionService, GrantedRoleMap, ModelPermissionService } from '@dereekb/model';
import { AbstractModelPermissionService, GrantedRoleMap, InContextModelPermissionService, ModelPermissionService } from '@dereekb/model';
import { Maybe, PromiseOrValue } from '@dereekb/util';
import { FirebasePermissionContext } from './permission.context';
import { FirebaseModelLoader } from '../model/model.loader';

export interface FirebasePermissionServiceModel<T, D extends FirestoreDocument<T> = FirestoreDocument<T>> {
readonly document: D;
readonly snapshot: DocumentSnapshot<T>;
readonly data: T;
readonly exists: boolean;
readonly data: Maybe<T>;
}

export type FirebaseModelPermissionService<C extends FirebasePermissionContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> = ModelPermissionService<D, C, R, FirebasePermissionServiceModel<T, D>>;
export type FirebaseModelPermissionService<C extends FirebasePermissionContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> = ModelPermissionService<C, D, R, FirebasePermissionServiceModel<T, D>>;

export interface FirebasePermissionServiceInstanceDelegate<C extends FirebasePermissionContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> extends FirebaseModelLoader<C, T, D> {
rolesMapForModel(output: FirebasePermissionServiceModel<T, D>, context: C, model: D): PromiseOrValue<GrantedRoleMap<R>>;
Expand All @@ -19,7 +20,7 @@ export interface FirebasePermissionServiceInstanceDelegate<C extends FirebasePer
/**
* Abstract AbstractModelPermissionService implementation for FirebaseModelsPermissionService.
*/
export class FirebaseModelPermissionServiceInstance<C extends FirebasePermissionContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> extends AbstractModelPermissionService<D, C, R, FirebasePermissionServiceModel<T, D>> implements FirebaseModelPermissionService<C, T, D, R> {
export class FirebaseModelPermissionServiceInstance<C extends FirebasePermissionContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> extends AbstractModelPermissionService<C, D, R, FirebasePermissionServiceModel<T, D>> implements FirebaseModelPermissionService<C, T, D, R> {
constructor(readonly delegate: FirebasePermissionServiceInstanceDelegate<C, T, D, R>) {
super(delegate);
}
Expand All @@ -32,11 +33,18 @@ export class FirebaseModelPermissionServiceInstance<C extends FirebasePermission
const snapshot = await document.accessor.get();
const data = snapshot.data();

const model: Maybe<FirebasePermissionServiceModel<T, D>> = data != null ? { document, snapshot, data } : undefined;
const model: Maybe<FirebasePermissionServiceModel<T, D>> = { document, snapshot, data, exists: data != null };
return model;
}

protected override isUsableOutputForRoles(output: FirebasePermissionServiceModel<T, D>) {
return output.exists;
}
}

export function firebaseModelPermissionService<C extends FirebasePermissionContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string>(delegate: FirebasePermissionServiceInstanceDelegate<C, T, D, R>): FirebaseModelPermissionServiceInstance<C, T, D, R> {
return new FirebaseModelPermissionServiceInstance(delegate);
}

// MARK: InContext
export type InContextFirebaseModelPermissionService<C extends FirebasePermissionContext, T, D extends FirestoreDocument<T> = FirestoreDocument<T>, R extends string = string> = InContextModelPermissionService<C, D, R, FirebasePermissionServiceModel<T, D>>;
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@ export class MockItemCollectionFixtureInstance {
return this.firestoreCollection.collection;
}
get firestoreCollection(): MockItemFirestoreCollection {
return this.collections.mockItem;
return this.collections.mockItemCollection;
}

get mockItemPrivateCollection() {
return this.collections.mockItemPrivate;
return this.collections.mockItemPrivateCollectionFactory;
}

get mockItemSubItemCollection() {
return this.collections.mockItemSubItem;
return this.collections.mockItemSubItemCollectionFactory;
}

get mockItemSubItemCollectionGroup() {
return this.collections.mockItemSubItemGroup;
return this.collections.mockItemSubItemCollectionGroup;
}

get mockItemDeepSubItemCollection() {
return this.collections.mockItemDeepSubItem;
return this.collections.mockItemDeepSubItemCollectionFactory;
}

get mockItemDeepSubItemCollectionGroup() {
return this.collections.mockItemDeepSubItemGroup;
return this.collections.mockItemDeepSubItemCollectionGroup;
}

constructor(readonly fixture: MockItemCollectionFixture) {}
Expand Down
Loading

0 comments on commit 7432e55

Please sign in to comment.