Skip to content

Commit

Permalink
feat: added SystemStateDocument
Browse files Browse the repository at this point in the history
- added FirestoreDataConverterFactory support to firestore accessor factory
- added example usage of system state
  • Loading branch information
dereekb committed Oct 9, 2022
1 parent 3b1a3ab commit d4a0fcf
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 10 deletions.
15 changes: 13 additions & 2 deletions apps/demo-api/src/app/function/example/example.schedule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { loadExampleSystemState } from '@dereekb/demo-firebase';
import { DemoScheduleFunction } from '../function';

export const exampleUsageOfSchedule: DemoScheduleFunction = (request) => {
console.log('exampleUsageOfSchedule() was called!');
export const exampleUsageOfSchedule: DemoScheduleFunction = async (request) => {
const exampleSystemStateDocument = loadExampleSystemState(request.nest.demoFirestoreCollections.systemStateCollection.documentAccessor());

const currentSystemState = await exampleSystemStateDocument.snapshotData();

console.log(`exampleUsageOfSchedule() was called! Last update was at ${currentSystemState?.data.lastUpdate}`);

await exampleSystemStateDocument.accessor.set({
data: {
lastUpdate: new Date()
}
});
};
11 changes: 9 additions & 2 deletions apps/demo-api/src/app/function/example/example.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { demoDevelopmentFunctionMap } from './../model/development.functions';
import { demoExampleUsageOfSchedule } from '../model/schedule.functions';
import { DemoDevelopmentExampleParams, DemoDevelopmentExampleResult, DEMO_APP_EXAMPLE_DEVELOPMENT_FUNCTION_SPECIFIER } from '@dereekb/demo-firebase';
import { DemoDevelopmentExampleParams, DemoDevelopmentExampleResult, DEMO_APP_EXAMPLE_DEVELOPMENT_FUNCTION_SPECIFIER, loadExampleSystemState } from '@dereekb/demo-firebase';
import { DemoApiFunctionContextFixture, demoApiFunctionContextFactory, demoAuthorizedUserContext } from '../../../test/fixture';
import { describeCloudFunctionTest } from '@dereekb/firebase-server/test';
import { onCallDevelopmentParams } from '@dereekb/firebase';
import { onCallDevelopmentFunction } from '@dereekb/firebase-server';
import { onCallWithDemoNestContext } from '../function';
import { isDate } from 'date-fns';

demoApiFunctionContextFactory((f: DemoApiFunctionContextFixture) => {
describeCloudFunctionTest('exampleUsageOfSchedule', { f, fns: { demoExampleUsageOfSchedule } }, ({ demoExampleUsageOfScheduleCloudFn }) => {
demoAuthorizedUserContext({ f }, (u) => {
it('should execute the scheduled task.', async () => {
const result = await u.callCloudFunction(demoExampleUsageOfScheduleCloudFn);

// expect no errors
// it should update the example system state
const exampleSystemStateDocument = loadExampleSystemState(f.instance.demoFirestoreCollections.systemStateCollection.documentAccessor());
const currentSystemState = await exampleSystemStateDocument.snapshotData();

expect(currentSystemState?.data).toBeDefined();
expect(currentSystemState?.data.lastUpdate).toBeDefined();
expect(isDate(currentSystemState?.data.lastUpdate)).toBe(true); // date should now be set with the latest date
});
});
});
Expand Down
1 change: 1 addition & 0 deletions components/demo-firebase/src/lib/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './guestbook';
export * from './profile';
export * from './service';
export * from './system';
12 changes: 10 additions & 2 deletions components/demo-firebase/src/lib/model/service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { FirebaseAppModelContext, firebaseModelServiceFactory, firebaseModelsService, FirebasePermissionServiceModel, FirestoreContext, grantFullAccessIfAdmin, grantFullAccessIfAuthUserRelated } from '@dereekb/firebase';
import { FirebaseAppModelContext, firebaseModelServiceFactory, firebaseModelsService, FirebasePermissionServiceModel, FirestoreContext, FirestoreDocumentAccessor, grantFullAccessIfAdmin, grantFullAccessIfAuthUserRelated, SystemState, SystemStateDocument, systemStateFirestoreCollection, SystemStateFirestoreCollection, SystemStateFirestoreCollections, SystemStateStoredData } from '@dereekb/firebase';
import { GrantedRoleMap } from '@dereekb/model';
import { PromiseOrValue } from '@dereekb/util';
import { GuestbookTypes, GuestbookFirestoreCollections, Guestbook, GuestbookDocument, GuestbookEntry, GuestbookEntryDocument, GuestbookEntryFirestoreCollectionFactory, GuestbookEntryFirestoreCollectionGroup, GuestbookEntryRoles, GuestbookFirestoreCollection, GuestbookRoles, guestbookEntryFirestoreCollectionFactory, guestbookEntryFirestoreCollectionGroup, guestbookFirestoreCollection } from './guestbook';
import { ProfileTypes, Profile, ProfileDocument, ProfileFirestoreCollection, ProfileFirestoreCollections, ProfilePrivateData, ProfilePrivateDataDocument, ProfilePrivateDataFirestoreCollectionFactory, ProfilePrivateDataFirestoreCollectionGroup, ProfilePrivateDataRoles, ProfileRoles, profileFirestoreCollection, profilePrivateDataFirestoreCollectionFactory, profilePrivateDataFirestoreCollectionGroup } from './profile';
import { demoSystemStateStoredDataConverterMap, ExampleSystemData, EXAMPLE_SYSTEM_DATA_SYSTEM_STATE_TYPE } from './system/system';

export abstract class DemoFirestoreCollections implements ProfileFirestoreCollections, GuestbookFirestoreCollections {
export abstract class DemoFirestoreCollections implements ProfileFirestoreCollections, GuestbookFirestoreCollections, SystemStateFirestoreCollections {
abstract readonly systemStateCollection: SystemStateFirestoreCollection;
abstract readonly guestbookCollection: GuestbookFirestoreCollection;
abstract readonly guestbookEntryCollectionGroup: GuestbookEntryFirestoreCollectionGroup;
abstract readonly guestbookEntryCollectionFactory: GuestbookEntryFirestoreCollectionFactory;
Expand All @@ -15,6 +17,7 @@ export abstract class DemoFirestoreCollections implements ProfileFirestoreCollec

export function makeDemoFirestoreCollections(firestoreContext: FirestoreContext): DemoFirestoreCollections {
return {
systemStateCollection: systemStateFirestoreCollection(firestoreContext, demoSystemStateStoredDataConverterMap),
guestbookCollection: guestbookFirestoreCollection(firestoreContext),
guestbookEntryCollectionGroup: guestbookEntryFirestoreCollectionGroup(firestoreContext),
guestbookEntryCollectionFactory: guestbookEntryFirestoreCollectionFactory(firestoreContext),
Expand Down Expand Up @@ -73,3 +76,8 @@ export type DemoFirebaseModelServiceFactories = typeof DEMO_FIREBASE_MODEL_SERVI
export const demoFirebaseModelServices = firebaseModelsService<DemoFirebaseModelServiceFactories, DemoFirebaseBaseContext, DemoFirebaseModelTypes>(DEMO_FIREBASE_MODEL_SERVICE_FACTORIES);

export type DemoFirebaseContext = DemoFirebaseBaseContext & { service: DemoFirebaseModelServiceFactories };

// MARK: System
export function loadExampleSystemState(accessor: FirestoreDocumentAccessor<SystemState<SystemStateStoredData>, SystemStateDocument<SystemStateStoredData>>): SystemStateDocument<ExampleSystemData> {
return accessor.loadDocumentForId(EXAMPLE_SYSTEM_DATA_SYSTEM_STATE_TYPE) as SystemStateDocument<ExampleSystemData>;
}
1 change: 1 addition & 0 deletions components/demo-firebase/src/lib/model/system/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './system';
19 changes: 19 additions & 0 deletions components/demo-firebase/src/lib/model/system/system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { firestoreSubObject, firestoreDate, snapshotConverterFunctions, SystemStateStoredData, SystemStateStoredDataFieldConverterConfig, SystemStateStoredDataConverterMap } from '@dereekb/firebase';

export const EXAMPLE_SYSTEM_DATA_SYSTEM_STATE_TYPE = 'example';

export interface ExampleSystemData extends SystemStateStoredData {
lastUpdate: Date;
}

export const exampleSystemDataConverter: SystemStateStoredDataFieldConverterConfig<ExampleSystemData> = firestoreSubObject<ExampleSystemData>({
objectField: {
fields: {
lastUpdate: firestoreDate({ saveDefaultAsNow: true })
}
}
});

export const demoSystemStateStoredDataConverterMap: SystemStateStoredDataConverterMap = {
[EXAMPLE_SYSTEM_DATA_SYSTEM_STATE_TYPE]: exampleSystemDataConverter
};
12 changes: 12 additions & 0 deletions packages/firebase/src/lib/common/firestore/accessor/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FactoryWithInput, FactoryWithRequiredInput, Maybe } from '@dereekb/util';
import { DocumentReference, FirestoreDataConverter } from '../types';

/**
* Factory used to provide an FirestoreDataConverter based on the input reference.
*/
export type FirestoreDataConverterFactory<T> = FactoryWithInput<FirestoreDataConverter<T>, DocumentReference<T>>;

/**
* Factory used to provide an optional custom FirestoreDataConverter based on the input reference.
*/
export type InterceptFirestoreDataConverterFactory<T> = FactoryWithRequiredInput<Maybe<FirestoreDataConverter<T>>, DocumentReference<T>>;
22 changes: 18 additions & 4 deletions packages/firebase/src/lib/common/firestore/accessor/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { lazyFrom } from '@dereekb/rxjs';
import { Observable } from 'rxjs';
import { FirestoreAccessorDriverRef } from '../driver/accessor';
import { FirestoreCollectionNameRef, FirestoreModelId, FirestoreModelIdentityCollectionName, FirestoreModelIdentityModelType, FirestoreModelIdentityRef, FirestoreModelIdRef, FirestoreModelKey, FirestoreModelKeyRef } from './../collection/collection';
import { DocumentReference, CollectionReference, Transaction, WriteBatch, DocumentSnapshot, SnapshotOptions, WriteResult, FirestoreDataConverter } from '../types';
import { DocumentReference, CollectionReference, Transaction, WriteBatch, DocumentSnapshot, SnapshotOptions, WriteResult, FirestoreDataConverter, DocumentData } from '../types';
import { FirestoreAccessorIncrementUpdate, dataFromSnapshotStream, FirestoreDocumentDataAccessor, FirestoreDocumentUpdateParams, updateWithAccessorUpdateAndConverterFunction } from './accessor';
import { CollectionReferenceRef, DocumentReferenceRef, FirestoreContextReference, FirestoreDataConverterRef } from '../reference';
import { FirestoreDocumentContext } from './context';
import { build, Maybe } from '@dereekb/util';
import { build, Factory, Maybe } from '@dereekb/util';
import { FirestoreModelTypeRef, FirestoreModelIdentity, FirestoreModelTypeModelIdentityRef } from '../collection/collection';
import { InterceptAccessorFactoryFunction } from './accessor.wrap';
import { incrementUpdateWithAccessorFunction } from './increment';
import { FirestoreDataConverterFactory, InterceptFirestoreDataConverterFactory } from './converter';

export interface FirestoreDocument<T, I extends FirestoreModelIdentity = FirestoreModelIdentity> extends FirestoreDataConverterRef<T>, DocumentReferenceRef<T>, CollectionReferenceRef<T>, FirestoreModelIdentityRef<I>, FirestoreModelTypeRef<FirestoreModelIdentityModelType<I>>, FirestoreCollectionNameRef<FirestoreModelIdentityCollectionName<I>>, FirestoreModelKeyRef, FirestoreModelIdRef {
readonly accessor: FirestoreDocumentDataAccessor<T>;
Expand Down Expand Up @@ -167,6 +168,11 @@ export interface LimitedFirestoreDocumentAccessor<T, D extends FirestoreDocument
* @param ref
*/
documentRefForKey(fullPath: FirestoreModelKey): DocumentReference<T>;

/**
* Returns the converter factory for this accessor.
*/
readonly converterFactory: FirestoreDataConverterFactory<T>;
}

export interface FirestoreDocumentAccessor<T, D extends FirestoreDocument<T> = FirestoreDocument<T>> extends LimitedFirestoreDocumentAccessor<T, D>, CollectionReferenceRef<T>, FirestoreAccessorDriverRef {
Expand Down Expand Up @@ -224,12 +230,17 @@ export interface LimitedFirestoreDocumentAccessorFactoryConfig<T, D extends Fire
* Optional InterceptAccessorFactoryFunction to intercept/return a modified accessor factory.
*/
readonly accessorFactory?: InterceptAccessorFactoryFunction<T>;
/**
* Optional InterceptFirestoreDataConverterFactory to return a modified converter.
*/
readonly converterFactory?: InterceptFirestoreDataConverterFactory<T>;
readonly makeDocument: FirestoreDocumentFactoryFunction<T, D>;
}

export function limitedFirestoreDocumentAccessorFactory<T, D extends FirestoreDocument<T> = FirestoreDocument<T>>(config: LimitedFirestoreDocumentAccessorFactoryConfig<T, D>): LimitedFirestoreDocumentAccessorFactoryFunction<T, D> {
const { firestoreContext, firestoreAccessorDriver, makeDocument, accessorFactory: interceptAccessorFactory, converter, modelIdentity } = config;
const { firestoreContext, firestoreAccessorDriver, makeDocument, accessorFactory: interceptAccessorFactory, converter: inputConverter, converterFactory: inputConverterFactory, modelIdentity } = config;
const expectedCollectionName = firestoreAccessorDriver.fuzzedPathForPath ? firestoreAccessorDriver.fuzzedPathForPath(modelIdentity.collectionName) : modelIdentity.collectionName;
const converterFactory: FirestoreDataConverterFactory<T> = inputConverterFactory ? (ref) => (ref ? inputConverterFactory(ref) ?? inputConverter : inputConverter) : () => inputConverter;

return (context?: FirestoreDocumentContext<T>) => {
const databaseContext: FirestoreDocumentContext<T> = context ?? config.firestoreAccessorDriver.defaultContextFactory();
Expand All @@ -240,6 +251,7 @@ export function limitedFirestoreDocumentAccessorFactory<T, D extends FirestoreDo
throw new Error('ref must be defined.');
}

const converter = converterFactory(ref);
const accessor = dataAccessorFactory.accessorFor(ref.withConverter(converter));
return makeDocument(accessor, documentAccessor);
}
Expand All @@ -251,6 +263,7 @@ export function limitedFirestoreDocumentAccessorFactory<T, D extends FirestoreDo
throw new Error(`unexpected key/path "${fullPath}" for expected type "${modelIdentity.collectionName}"/"${modelIdentity.modelType}".`);
}

const converter = converterFactory(ref);
return ref.withConverter(converter);
}

Expand All @@ -260,7 +273,8 @@ export function limitedFirestoreDocumentAccessorFactory<T, D extends FirestoreDo
}

const documentAccessor: LimitedFirestoreDocumentAccessor<T, D> = {
converter,
converter: inputConverter,
converterFactory,
modelIdentity,
loadDocumentFrom(document: FirestoreDocument<T>): D {
return loadDocument(document.documentRef);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './context.batch';
export * from './context.default';
export * from './context.transaction';
export * from './context';
export * from './converter';
export * from './document';
export * from './document.utility';
export * from './document.rxjs';
Expand Down
1 change: 1 addition & 0 deletions packages/firebase/src/lib/model/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './user';
export * from './system';
1 change: 1 addition & 0 deletions packages/firebase/src/lib/model/system/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './system';
100 changes: 100 additions & 0 deletions packages/firebase/src/lib/model/system/system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { GrantedSysAdminRole } from '@dereekb/model';
import { AbstractFirestoreDocument } from '../../common/firestore/accessor/document';
import { FirestoreCollection, firestoreModelIdentity } from '../../common/firestore/collection/collection';
import { FirestoreContext } from '../../common/firestore/context';
import { snapshotConverterFunctions } from '../../common/firestore/snapshot/snapshot';
import { CollectionReference } from '../../common/firestore/types';
import { firestorePassThroughField } from '../../common/firestore/snapshot/snapshot.field';
import { mapObjectMap, ModelFieldMapFunctionsConfig, cachedGetter } from '@dereekb/util';

// MARK: Collection
export interface SystemStateFirestoreCollections {
readonly systemStateCollection: SystemStateFirestoreCollection;
}

export type SystemStateTypes = typeof systemStateIdentity;

// MARK: Mock Item
export const systemStateIdentity = firestoreModelIdentity('systemState', 'sys');

/**
* Used to identify a SystemStateId.
*/
export type SystemStateTypeIdentifier = string;

/**
* Used to identify a SystemStateId.
*/
export type SystemStateId = SystemStateTypeIdentifier;

/**
* Arbitrary data stored within a SystemState. Stored values should always be either a string, number, or boolean.
*/
export type SystemStateStoredData = Record<string, any>;

/**
* A collection used for recording the current state of system subcomponents. System states for a given identifier are treated as a system-wide singleton/state/setting.
*
* For example, a SystemState with a specific SystemStateId may be relied on for information about the previous update, etc.
*/
export interface SystemState<T extends SystemStateStoredData = SystemStateStoredData> {
data: T;
}

export type SystemStateRoles = GrantedSysAdminRole;

/**
* Refers to a singleton SystemState based on this model's identifier.
*/
export class SystemStateDocument<T extends SystemStateStoredData = SystemStateStoredData> extends AbstractFirestoreDocument<SystemState<T>, SystemStateDocument<T>, typeof systemStateIdentity> {
get modelIdentity() {
return systemStateIdentity;
}
}

export const systemStateConverter = snapshotConverterFunctions<SystemState>({
fields: {
data: firestorePassThroughField()
}
});

export function systemStateCollectionReference(context: FirestoreContext): CollectionReference<SystemState> {
return context.collection(systemStateIdentity.collectionName);
}

export type SystemStateFirestoreCollection = FirestoreCollection<SystemState, SystemStateDocument>;

/**
* A ModelFieldMapFunctionsConfig used for data conversion.
*/
export type SystemStateStoredDataFieldConverterConfig<T extends SystemStateStoredData = SystemStateStoredData> = ModelFieldMapFunctionsConfig<T, any>;

export type SystemStateStoredDataConverterMap = {
[key: string]: SystemStateStoredDataFieldConverterConfig<any>;
};

export function systemStateFirestoreCollection(firestoreContext: FirestoreContext, converters: SystemStateStoredDataConverterMap): SystemStateFirestoreCollection {
const mappedConvertersGetter = cachedGetter(() =>
mapObjectMap(converters, (dataConverter) => {
return snapshotConverterFunctions<SystemState>({
fields: {
data: dataConverter
}
});
})
);

return firestoreContext.firestoreCollection({
converter: systemStateConverter,
converterFactory: (ref) => {
const type: SystemStateTypeIdentifier = ref.id;
return mappedConvertersGetter()[type];
},
modelIdentity: systemStateIdentity,
collection: systemStateCollectionReference(firestoreContext),
makeDocument: (a, d) => {
return new SystemStateDocument(a, d);
},
firestoreContext
});
}

0 comments on commit d4a0fcf

Please sign in to comment.