Skip to content

Commit

Permalink
feat: added interceptAccessorFactory()
Browse files Browse the repository at this point in the history
- removed FirestoreDocumentDataAccessor typing from FirestoreDocument
- added modifyModelMapFunctions/modifiers to snapshot converter
- added AbstractFirestoreDocumentDataAccessorWrapper
- added ModifyBeforeSetFirestoreDocumentDataAccessorWrapper and related content
- added copyUserRelatedDataAccessorFactoryFunction()
- added tests for copyUserRelatedDataAccessorFactoryFunction() usage
- added mockItemUser
- flattenArray() now uses flat() to flatten, instead of accumulator
  • Loading branch information
dereekb committed Jun 6, 2022
1 parent 118bde7 commit 9833539
Show file tree
Hide file tree
Showing 16 changed files with 489 additions and 36 deletions.
36 changes: 32 additions & 4 deletions components/demo-firebase/src/lib/models/guestbook/guestbook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
import { CollectionReference, AbstractFirestoreDocument, snapshotConverterFunctions, firestoreString, firestoreDate, FirestoreCollection, UserRelatedById, DocumentReferenceRef, FirestoreContext, FirestoreCollectionWithParent, firestoreBoolean, DocumentDataWithId, AbstractFirestoreDocumentWithParent, optionalFirestoreDate, FirestoreCollectionGroup, CollectionGroup, firestoreModelIdentity } from '@dereekb/firebase';
import {
CollectionReference,
AbstractFirestoreDocument,
snapshotConverterFunctions,
firestoreString,
firestoreDate,
FirestoreCollection,
UserRelatedById,
DocumentReferenceRef,
FirestoreContext,
FirestoreCollectionWithParent,
firestoreBoolean,
DocumentDataWithId,
AbstractFirestoreDocumentWithParent,
optionalFirestoreDate,
FirestoreCollectionGroup,
CollectionGroup,
firestoreModelIdentity,
UserRelated,
FirestoreModelNames,
modifyBeforeSetInterceptAccessorFactoryFunction,
copyUserRelatedDataModifierConfig,
copyUserRelatedDataAccessorFactoryFunction
} from '@dereekb/firebase';
import { GrantedReadRole } from '@dereekb/model';
import { Maybe } from '@dereekb/util';

Expand Down Expand Up @@ -71,7 +94,7 @@ export function guestbookFirestoreCollection(firestoreContext: FirestoreContext)
// MARK: Guestbook Entry
export const guestbookEntryIdentity = firestoreModelIdentity('guestbookEntry');

export interface GuestbookEntry extends UserRelatedById {
export interface GuestbookEntry extends UserRelated, UserRelatedById {
/**
* Guestbook message.
*/
Expand Down Expand Up @@ -104,8 +127,9 @@ export class GuestbookEntryDocument extends AbstractFirestoreDocumentWithParent<

export const guestbookEntryConverter = snapshotConverterFunctions<GuestbookEntry>({
fields: {
message: firestoreString({ default: '' }),
signed: firestoreString({ default: '' }),
uid: firestoreString(),
message: firestoreString(),
signed: firestoreString(),
updatedAt: firestoreDate({ saveDefaultAsNow: true }),
createdAt: firestoreDate({ saveDefaultAsNow: true }),
published: firestoreBoolean({ default: false, defaultBeforeSave: false })
Expand All @@ -118,6 +142,8 @@ export function guestbookEntryCollectionReferenceFactory(context: FirestoreConte
};
}

export const guestbookEntryAccessorFactory = copyUserRelatedDataAccessorFactoryFunction<GuestbookEntry>();

export type GuestbookEntryFirestoreCollection = FirestoreCollectionWithParent<GuestbookEntry, Guestbook, GuestbookEntryDocument, GuestbookDocument>;
export type GuestbookEntryFirestoreCollectionFactory = (parent: GuestbookDocument) => GuestbookEntryFirestoreCollection;

Expand All @@ -128,6 +154,7 @@ export function guestbookEntryFirestoreCollectionFactory(firestoreContext: Fires
return firestoreContext.firestoreCollectionWithParent({
itemsPerPage: 50,
collection: factory(parent),
accessorFactory: guestbookEntryAccessorFactory,
makeDocument: (accessor, documentAccessor) => new GuestbookEntryDocument(accessor, documentAccessor),
firestoreContext,
parent
Expand All @@ -144,6 +171,7 @@ export type GuestbookEntryFirestoreCollectionGroup = FirestoreCollectionGroup<Gu
export function guestbookEntryFirestoreCollectionGroup(firestoreContext: FirestoreContext): GuestbookEntryFirestoreCollectionGroup {
return firestoreContext.firestoreCollectionGroup({
itemsPerPage: 50,
accessorFactory: guestbookEntryAccessorFactory,
queryLike: guestbookEntryCollectionReference(firestoreContext),
makeDocument: (accessor, documentAccessor) => new GuestbookEntryDocument(accessor, documentAccessor),
firestoreContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export interface FirestoreDocumentDataAccessor<T, D = DocumentData> extends Docu
update(data: UpdateData<D>, params?: FirestoreDocumentUpdateParams): Promise<WriteResult | void>;
}

export type FirestoreDocumentDataAccessorSetFunction<T> = (data: PartialWithFieldValue<T> | WithFieldValue<T>, options?: SetOptions) => Promise<void | WriteResult>;

/**
* Contextual interface used for making a FirestoreDocumentModifier for a specific document.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { ArrayOrValue, asArray, mergeModifiers, ModifierFunction, cachedGetter } from '@dereekb/util';
import { UserRelated } from '../../../model/user';
import { DocumentReferenceRef } from '../reference';
import { SetOptionsMerge, SetOptionsMergeFields, DocumentData, PartialWithFieldValue, SetOptions, WithFieldValue } from '../types';
import { FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorSetFunction } from './accessor';
import { AbstractFirestoreDocumentDataAccessorWrapper, interceptAccessorFactoryFunction, InterceptAccessorFactoryFunction } from './accessor.wrap';

// MARK: Set Wrapper
export type ModifyBeforeSetFistoreDataAccessorMode = 'always' | 'update' | 'set';

/**
* Input fora ModifyBeforeSetFirestoreDocumentDataAccessorWrapper
*/
export interface ModifyBeforeSetFistoreDataAccessorInput<T> extends DocumentReferenceRef<T> {
/**
* Data to pass to the modifyAndSet function.
*/
readonly data: Partial<T>;
/**
* Set options passed to the set function, if available.
*/
readonly options?: SetOptions;
}

export type ModifyBeforeSetModifierFunction<T> = ModifierFunction<ModifyBeforeSetFistoreDataAccessorInput<T>>;

export interface ModifyBeforeSetConfig<T extends object> {
/**
* When to modify the input data.
*/
readonly when: ModifyBeforeSetFistoreDataAccessorMode;
/**
* Modifier or array of modifier functions to apply to input data.
*/
readonly modifier: ArrayOrValue<ModifyBeforeSetModifierFunction<T>>;
}

/**
* FirestoreDocumentDataAccessorWrapper that applies a modifier function to data being set. When the modifier functions are applied can be changed by the mode.
*/
export class ModifyBeforeSetFirestoreDocumentDataAccessorWrapper<T extends object, D = DocumentData> extends AbstractFirestoreDocumentDataAccessorWrapper<T, D> {
readonly modifier: ModifierFunction<ModifyBeforeSetFistoreDataAccessorInput<T>>;
readonly set: FirestoreDocumentDataAccessorSetFunction<T>;

constructor(accessor: FirestoreDocumentDataAccessor<T, D>, readonly config: ModifyBeforeSetConfig<T>) {
super(accessor);
const when = config.when;
this.modifier = mergeModifiers(asArray(config.modifier));

let setFn: FirestoreDocumentDataAccessorSetFunction<T>;

const modifyAndSet: FirestoreDocumentDataAccessorSetFunction<T> = (data: PartialWithFieldValue<T> | WithFieldValue<T>, options?: SetOptions) => {
const copy = { ...data };
const input: ModifyBeforeSetFistoreDataAccessorInput<T> = {
data: copy,
documentRef: this.documentRef,
options
};

this.modifier(input);
return super.set(input.data, options as SetOptions);
};

switch (when) {
case 'always':
setFn = modifyAndSet;
break;
case 'set':
setFn = (data: PartialWithFieldValue<T> | WithFieldValue<T>, options?: SetOptions) => {
const isSetForNewModel = Boolean(!options);
if (isSetForNewModel) {
return modifyAndSet(data);
} else {
return super.set(data, options as SetOptions);
}
};
break;
case 'update':
setFn = (data: PartialWithFieldValue<T> | WithFieldValue<T>, options?: SetOptions) => {
const isUpdateForExistingModel = options && (Boolean((options as SetOptionsMergeFields).mergeFields) || Boolean((options as SetOptionsMerge).merge));
if (isUpdateForExistingModel) {
return modifyAndSet(data);
} else {
return super.set(data, options as SetOptions);
}
};
break;
}

this.set = setFn;
}
}

// MARK: Modifier Functions
/**
* Creates a ModifyBeforeSetModifierFunction<T> to copy the documentRef's id to the target field on the data.
*
* @param fieldName
* @returns
*/
export function copyDocumentIdToFieldModifierFunction<T extends object>(fieldName: keyof T): ModifyBeforeSetModifierFunction<T> {
return ({ data, documentRef }) => {
(data as unknown as Record<any, string>)[fieldName] = documentRef.id; // copy the id to the target field
};
}

export function modifyBeforeSetInterceptAccessorFactoryFunction<T extends object, D = DocumentData>(config: ModifyBeforeSetConfig<T>): InterceptAccessorFactoryFunction<T, D> {
return interceptAccessorFactoryFunction((accessor) => new ModifyBeforeSetFirestoreDocumentDataAccessorWrapper(accessor, config));
}

// MARK: Templates
export function copyDocumentIdForUserRelatedModifierFunction<T extends UserRelated>(): ModifyBeforeSetModifierFunction<T> {
return copyDocumentIdToFieldModifierFunction<T>('uid');
}

/**
* Returns a pre-configured ModifyBeforeSetConfig<T> for UserRelated models
* @returns
*/
export function copyUserRelatedDataModifierConfig<T extends UserRelated>(): ModifyBeforeSetConfig<T> {
return {
when: 'set',
modifier: copyDocumentIdForUserRelatedModifierFunction()
};
}

export const COPY_USER_RELATED_DATA_ACCESSOR_FACTORY_FUNCTION = cachedGetter(() => modifyBeforeSetInterceptAccessorFactoryFunction(copyUserRelatedDataModifierConfig()));

export function copyUserRelatedDataAccessorFactoryFunction<T extends UserRelated, D = DocumentData>(): InterceptAccessorFactoryFunction<T, D> {
return COPY_USER_RELATED_DATA_ACCESSOR_FACTORY_FUNCTION() as InterceptAccessorFactoryFunction<T, D>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Observable } from 'rxjs';
import { DocumentData, DocumentReference, DocumentSnapshot, PartialWithFieldValue, SetOptions, UpdateData, WithFieldValue, WriteResult } from '../types';
import { FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentDeleteParams, FirestoreDocumentUpdateParams } from './accessor';

// MARK: Abstract Wrapper
/**
* Abstract wrapper for a FirestoreDocumentDataAccessor.
*
* Forwards all non-overridden accessor functions to the wrapped accessor by default.
*/
export abstract class AbstractFirestoreDocumentDataAccessorWrapper<T, D = DocumentData> implements FirestoreDocumentDataAccessor<T, D> {
constructor(readonly accessor: FirestoreDocumentDataAccessor<T, D>) {}

get documentRef(): DocumentReference<T> {
return this.accessor.documentRef;
}

stream(): Observable<DocumentSnapshot<T>> {
return this.accessor.stream();
}

get(): Promise<DocumentSnapshot<T>> {
return this.accessor.get();
}

exists(): Promise<boolean> {
return this.accessor.exists();
}

delete(params?: FirestoreDocumentDeleteParams): Promise<void | WriteResult> {
return this.accessor.delete(params);
}

set(data: PartialWithFieldValue<T>, options: SetOptions): Promise<WriteResult | void>;
set(data: WithFieldValue<T>): Promise<WriteResult | void>;
set(data: PartialWithFieldValue<T> | WithFieldValue<T>, options?: SetOptions): Promise<void | WriteResult> {
return this.accessor.set(data, options as SetOptions);
}

update(data: UpdateData<D>, params?: FirestoreDocumentUpdateParams): Promise<void | WriteResult> {
return this.update(data, params);
}
}

// MARK: Factory
export type WrapFirestoreDocumentDataAccessorFunction<T, D = DocumentData> = (input: FirestoreDocumentDataAccessor<T, D>) => FirestoreDocumentDataAccessor<T, D>;
export type InterceptAccessorFactoryFunction<T, D = DocumentData> = (input: FirestoreDocumentDataAccessorFactory<T, D>) => FirestoreDocumentDataAccessorFactory<T, D>;

export function interceptAccessorFactoryFunction<T, D = DocumentData>(wrap: WrapFirestoreDocumentDataAccessorFunction<T, D>): InterceptAccessorFactoryFunction<T, D> {
return (input: FirestoreDocumentDataAccessorFactory<T, D>) => ({
accessorFor: (ref) => wrap(input.accessorFor(ref))
});
}
27 changes: 16 additions & 11 deletions packages/firebase/src/lib/common/firestore/accessor/document.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { FirestoreModelId, FirestoreModelIdRef, FirestoreModelKey, FirestoreModelKeyRef, FirestoreModelName } from './../collection/collection';
/*eslint @typescript-eslint/no-explicit-any:"off"*/
// any is used with intent here, as the recursive AbstractFirestoreDocument requires its use to terminate.

import { Observable } from 'rxjs';
import { FirestoreAccessorDriverRef } from '../driver/accessor';
import { FirestoreModelId, FirestoreModelIdRef, FirestoreModelKey, FirestoreModelKeyRef, FirestoreModelName } from './../collection/collection';
import { DocumentReference, CollectionReference, Transaction, WriteBatch, DocumentSnapshot, SnapshotOptions, WriteResult } from '../types';
import { createOrUpdateWithAccessorSet, dataFromSnapshotStream, FirestoreDocumentDataAccessor } from './accessor';
import { CollectionReferenceRef, DocumentReferenceRef, FirestoreContextReference } from '../reference';
import { FirestoreDocumentContext } from './context';
import { build } from '@dereekb/util';
import { FirestoreModelNameRef, FirestoreModelIdentity, FirestoreModelIdentityRef } from '../collection/collection';
import { InterceptAccessorFactoryFunction } from './accessor.wrap';

export interface FirestoreDocument<T, A extends FirestoreDocumentDataAccessor<T> = FirestoreDocumentDataAccessor<T>, M extends FirestoreModelName = FirestoreModelName> extends DocumentReferenceRef<T>, CollectionReferenceRef<T>, FirestoreModelIdentityRef<M>, FirestoreModelNameRef<M>, FirestoreModelKeyRef, FirestoreModelIdRef {
readonly accessor: A;
export interface FirestoreDocument<T, M extends FirestoreModelName = FirestoreModelName> extends DocumentReferenceRef<T>, CollectionReferenceRef<T>, FirestoreModelIdentityRef<M>, FirestoreModelNameRef<M>, FirestoreModelKeyRef, FirestoreModelIdRef {
readonly accessor: FirestoreDocumentDataAccessor<T>;
readonly id: string;
}

/**
* Abstract FirestoreDocument implementation that extends a FirestoreDocumentDataAccessor.
*/
export abstract class AbstractFirestoreDocument<T, D extends AbstractFirestoreDocument<T, any, any>, A extends FirestoreDocumentDataAccessor<T> = FirestoreDocumentDataAccessor<T>, M extends FirestoreModelName = FirestoreModelName> implements FirestoreDocument<T, A>, LimitedFirestoreDocumentAccessorRef<T, D>, CollectionReferenceRef<T> {
export abstract class AbstractFirestoreDocument<T, D extends AbstractFirestoreDocument<T, any, M>, M extends FirestoreModelName = FirestoreModelName> implements FirestoreDocument<T>, LimitedFirestoreDocumentAccessorRef<T, D>, CollectionReferenceRef<T> {
readonly stream$ = this.accessor.stream();
readonly data$: Observable<T> = dataFromSnapshotStream(this.stream$);

constructor(readonly accessor: A, readonly documentAccessor: LimitedFirestoreDocumentAccessor<T, D>) {}
constructor(readonly accessor: FirestoreDocumentDataAccessor<T>, readonly documentAccessor: LimitedFirestoreDocumentAccessor<T, D>) {}

abstract get modelIdentity(): FirestoreModelIdentity<M>;

Expand Down Expand Up @@ -172,19 +173,23 @@ export interface LimitedFirestoreDocumentAccessorFactory<T, D extends FirestoreD
* FirestoreDocumentAccessor configuration.
*/
export interface LimitedFirestoreDocumentAccessorFactoryConfig<T, D extends FirestoreDocument<T> = FirestoreDocument<T>> extends FirestoreContextReference, FirestoreAccessorDriverRef {
/**
* Optional InterceptAccessorFactoryFunction to intercept/return a modified accessor factory.
*/
readonly accessorFactory?: InterceptAccessorFactoryFunction<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 } = config;
const { firestoreContext, firestoreAccessorDriver, makeDocument, accessorFactory: interceptAccessorFactory } = config;

return (context?: FirestoreDocumentContext<T>) => {
const databaseContext: FirestoreDocumentContext<T> = context ?? config.firestoreAccessorDriver.defaultContextFactory();
const dataAccessorFactory = databaseContext.accessorFactory;
const dataAccessorFactory = interceptAccessorFactory ? interceptAccessorFactory(databaseContext.accessorFactory) : databaseContext.accessorFactory;

function loadDocument(ref: DocumentReference<T>) {
const accessor = dataAccessorFactory.accessorFor(ref);
return config.makeDocument(accessor, documentAccessor);
return makeDocument(accessor, documentAccessor);
}

function documentRefForKey(fullPath: FirestoreModelKey): DocumentReference<T> {
Expand Down Expand Up @@ -311,16 +316,16 @@ export function firestoreDocumentAccessorContextExtension<T, D extends Firestore
}

// MARK: Document With Parent (Subcollection Items)
export interface FirestoreDocumentWithParent<P, T, A extends FirestoreDocumentDataAccessor<T> = FirestoreDocumentDataAccessor<T>> extends FirestoreDocument<T, A> {
export interface FirestoreDocumentWithParent<P, T> extends FirestoreDocument<T> {
readonly parent: DocumentReference<P>;
}

export abstract class AbstractFirestoreDocumentWithParent<P, T, D extends AbstractFirestoreDocument<T, any, any>, A extends FirestoreDocumentDataAccessor<T> = FirestoreDocumentDataAccessor<T>> extends AbstractFirestoreDocument<T, D, A> implements FirestoreDocumentWithParent<P, T, A> {
export abstract class AbstractFirestoreDocumentWithParent<P, T, D extends AbstractFirestoreDocument<T, any>> extends AbstractFirestoreDocument<T, D> implements FirestoreDocumentWithParent<P, T> {
get parent() {
return (this.accessor.documentRef.parent as CollectionReference<T>).parent as DocumentReference<P>;
}

constructor(accessor: A, documentAccessor: LimitedFirestoreDocumentAccessor<T, D>) {
constructor(accessor: FirestoreDocumentDataAccessor<T>, documentAccessor: LimitedFirestoreDocumentAccessor<T, D>) {
super(accessor, documentAccessor);
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/firebase/src/lib/common/firestore/accessor/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from './accessor.batch';
export * from './accessor.default';
export * from './accessor.transaction';
export * from './accessor.wrap';
export * from './accessor.wrap.modify';
export * from './accessor';
export * from './context.batch';
export * from './context.default';
Expand Down
Loading

0 comments on commit 9833539

Please sign in to comment.