Skip to content

Commit

Permalink
feat: added getWithConverter()
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Jun 17, 2022
1 parent b8971f2 commit aef4b27
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DocumentReference, WriteBatch as GoogleCloudWriteBatch, DocumentSnapshot } from '@google-cloud/firestore';
import { from, Observable } from 'rxjs';
import { WithFieldValue, FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentDeleteParams, FirestoreDocumentUpdateParams, UpdateData } from '@dereekb/firebase';
import { WithFieldValue, FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentDeleteParams, FirestoreDocumentUpdateParams, UpdateData, DocumentData, FirestoreDataConverter } from '@dereekb/firebase';

// MARK: Accessor
/**
Expand All @@ -26,6 +26,10 @@ export class WriteBatchFirestoreDocumentDataAccessor<T> implements FirestoreDocu
return this.documentRef.get();
}

getWithConverter<U = DocumentData>(converter: null | FirestoreDataConverter<U>): Promise<DocumentSnapshot<U>> {
return this.documentRef.withConverter<U>(converter as FirestoreDataConverter<U>).get();
}

delete(params?: FirestoreDocumentDeleteParams): Promise<void> {
this.batch.delete(this.documentRef, params?.precondition);
return Promise.resolve();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DocumentReference, WriteResult as GoogleCloudWriteResult, DocumentSnapshot } from '@google-cloud/firestore';
import { Observable } from 'rxjs';
import { WithFieldValue, UpdateData, FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentDeleteParams, FirestoreDocumentUpdateParams, SetOptions, streamFromOnSnapshot } from '@dereekb/firebase';
import { WithFieldValue, UpdateData, FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentDeleteParams, FirestoreDocumentUpdateParams, SetOptions, streamFromOnSnapshot, FirestoreDataConverter, DocumentData } from '@dereekb/firebase';

// MARK: Accessor
export class DefaultFirestoreDocumentDataAccessor<T> implements FirestoreDocumentDataAccessor<T> {
Expand All @@ -22,6 +22,10 @@ export class DefaultFirestoreDocumentDataAccessor<T> implements FirestoreDocumen
return this.documentRef.get();
}

getWithConverter<U = DocumentData>(converter: null | FirestoreDataConverter<U>): Promise<DocumentSnapshot<U>> {
return this.documentRef.withConverter<U>(converter as FirestoreDataConverter<U>).get();
}

delete(params: FirestoreDocumentDeleteParams): Promise<GoogleCloudWriteResult> {
return this.documentRef.delete(params?.precondition);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DocumentReference, DocumentSnapshot, Transaction as GoogleCloudTransaction, SetOptions } from '@google-cloud/firestore';
import { from, Observable } from 'rxjs';
import { WithFieldValue, UpdateData, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentUpdateParams } from '@dereekb/firebase';
import { WithFieldValue, UpdateData, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentUpdateParams, FirestoreDataConverter, DocumentData } from '@dereekb/firebase';

// MARK: Accessor
/**
Expand All @@ -26,6 +26,10 @@ export class TransactionFirestoreDocumentDataAccessor<T> implements FirestoreDoc
return this.transaction.get(this.documentRef);
}

getWithConverter<U = DocumentData>(converter: null | FirestoreDataConverter<U>): Promise<DocumentSnapshot<U>> {
return this.transaction.get(this.documentRef.withConverter<U>(converter as FirestoreDataConverter<U>));
}

delete(): Promise<void> {
this.transaction.delete(this.documentRef);
return Promise.resolve();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,28 @@ import { DocumentReference, DocumentSnapshot, getDoc, WriteBatch as FirebaseFire
import { from, Observable } from 'rxjs';
import { FirestoreDocumentContext, UpdateData, WithFieldValue, FirestoreDocumentContextType, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, SetOptions } from '../../common/firestore';
import { createWithAccessor } from './driver.accessor.create';
import { DefaultFirestoreDocumentDataAccessor } from './driver.accessor.default';

// MARK: Accessor
/**
* FirestoreDocumentDataAccessor implementation for a batch.
*/
export class WriteBatchFirestoreDocumentDataAccessor<T> implements FirestoreDocumentDataAccessor<T> {
constructor(readonly batch: FirebaseFirestoreWriteBatch, readonly documentRef: DocumentReference<T>) {}

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

create(data: WithFieldValue<T>): Promise<void> {
return createWithAccessor(this)(data) as Promise<void>;
}

exists(): Promise<boolean> {
return this.get().then((x) => x.exists());
}

get(): Promise<DocumentSnapshot<T>> {
return getDoc(this.documentRef);
export class WriteBatchFirestoreDocumentDataAccessor<T> extends DefaultFirestoreDocumentDataAccessor<T> implements FirestoreDocumentDataAccessor<T> {
constructor(readonly batch: FirebaseFirestoreWriteBatch, documentRef: DocumentReference<T>) {
super(documentRef);
}

delete(): Promise<void> {
override delete(): Promise<void> {
this.batch.delete(this.documentRef);
return Promise.resolve();
}

set(data: WithFieldValue<T>, options?: SetOptions): Promise<void> {
override set(data: WithFieldValue<T>, options?: SetOptions): Promise<void> {
this.batch.set(this.documentRef, data, options as SetOptions);
return Promise.resolve();
}

update(data: UpdateData<unknown>): Promise<void> {
override update(data: UpdateData<unknown>): Promise<void> {
this.batch.update(this.documentRef, data as FirestoreUpdateData<T>);
return Promise.resolve();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DocumentReference, DocumentSnapshot, UpdateData, WithFieldValue, getDoc, deleteDoc, setDoc, updateDoc } from '@firebase/firestore';
import { fromRef } from 'rxfire/firestore';
import { Observable } from 'rxjs';
import { FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, SetOptions } from '../../common/firestore';
import { DocumentData, FirestoreDataConverter, FirestoreDocumentContext, FirestoreDocumentContextType, FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, SetOptions } from '../../common/firestore';
import { createWithAccessor } from './driver.accessor.create';

// MARK: Accessor
Expand All @@ -24,6 +24,10 @@ export class DefaultFirestoreDocumentDataAccessor<T> implements FirestoreDocumen
return getDoc(this.documentRef);
}

getWithConverter<U = DocumentData>(converter: null | FirestoreDataConverter<U>): Promise<DocumentSnapshot<U>> {
return getDoc(this.documentRef.withConverter<U>(converter as FirestoreDataConverter<U>)) as Promise<DocumentSnapshot<U>>;
}

delete(): Promise<void> {
return deleteDoc(this.documentRef);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DocumentReference, DocumentSnapshot, Transaction as FirebaseFirestoreTransaction, UpdateData, WithFieldValue } from '@firebase/firestore';
import { from, Observable } from 'rxjs';
import { FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentContext, FirestoreDocumentContextType, SetOptions } from '../../common/firestore';
import { FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentContext, FirestoreDocumentContextType, SetOptions, DocumentData, FirestoreDataConverter } from '../../common/firestore';
import { createWithAccessor } from './driver.accessor.create';

// MARK: Accessor
Expand All @@ -26,6 +26,10 @@ export class TransactionFirestoreDocumentDataAccessor<T> implements FirestoreDoc
return this.transaction.get(this.documentRef);
}

getWithConverter<U = DocumentData>(converter: null | FirestoreDataConverter<U>): Promise<DocumentSnapshot<U>> {
return this.transaction.get(this.documentRef.withConverter<U>(converter as FirestoreDataConverter<U>)) as Promise<DocumentSnapshot<U>>;
}

delete(): Promise<void> {
this.transaction.delete(this.documentRef);
return Promise.resolve();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { filterMaybe } from '@dereekb/rxjs';
import { filterUndefinedValues, Maybe } from '@dereekb/util';
import { WriteResult, SnapshotOptions, DocumentReference, DocumentSnapshot, UpdateData, WithFieldValue, PartialWithFieldValue, SetOptions, Precondition, DocumentData } from '../types';
import { WriteResult, SnapshotOptions, DocumentReference, DocumentSnapshot, UpdateData, WithFieldValue, PartialWithFieldValue, SetOptions, Precondition, DocumentData, FirestoreDataConverter } from '../types';
import { map, Observable, OperatorFunction } from 'rxjs';
import { DocumentReferenceRef } from '../reference';

Expand Down Expand Up @@ -28,6 +28,13 @@ export interface FirestoreDocumentDataAccessor<T, D = DocumentData> extends Docu
* Returns the current snapshot.
*/
get(): Promise<DocumentSnapshot<T>>;
/**
* Gets the data from the datastore using the input converter.
*
* @param converter
*/
getWithConverter(converter: null): Promise<DocumentSnapshot<DocumentData>>;
getWithConverter<U = DocumentData>(converter: null | FirestoreDataConverter<U>): Promise<DocumentSnapshot<U>>;
/**
* Whether or not the target object currently exists.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Observable } from 'rxjs';
import { DocumentData, DocumentReference, DocumentSnapshot, PartialWithFieldValue, SetOptions, UpdateData, WithFieldValue, WriteResult } from '../types';
import { DocumentData, DocumentReference, DocumentSnapshot, FirestoreDataConverter, PartialWithFieldValue, SetOptions, UpdateData, WithFieldValue, WriteResult } from '../types';
import { FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, FirestoreDocumentDeleteParams, FirestoreDocumentUpdateParams } from './accessor';

// MARK: Abstract Wrapper
Expand All @@ -10,7 +10,6 @@ import { FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, Fi
*/
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;
}
Expand All @@ -27,6 +26,10 @@ export abstract class AbstractFirestoreDocumentDataAccessorWrapper<T, D = Docume
return this.accessor.get();
}

getWithConverter<U = DocumentData>(converter: null | FirestoreDataConverter<U>): Promise<DocumentSnapshot<U>> {
return this.accessor.getWithConverter(converter);
}

exists(): Promise<boolean> {
return this.accessor.exists();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { firestoreBoolean, firestoreDate, firestoreString, firestoreUniqueString
export interface TestSnapshotDefaults {
date: Date;
uniqueStringArray: string[];
uniqueStringArrayWithDefaultValue: string[];
}

export const testSnapshotDefaultsConverter = snapshotConverterFunctions<TestSnapshotDefaults>({
fields: {
date: firestoreDate({ saveDefaultAsNow: true }),
uniqueStringArray: firestoreUniqueStringArray()
uniqueStringArray: firestoreUniqueStringArray(),
uniqueStringArrayWithDefaultValue: firestoreUniqueStringArray({ default: () => ['test'] })
}
});

Expand Down Expand Up @@ -61,6 +63,17 @@ describe('snapshotConverterFunctions()', () => {
expect(Array.isArray(result.uniqueStringArray)).toBe(true);
});

it('should apply default values when converting from an object with null values', () => {
const data = { uniqueStringArray: null, uniqueStringArrayWithDefaultValue: null };
const result = testSnapshotDefaultsConverter.from(testSnapshotDefaultsSnapshotData(data as any));

expect(result.date).not.toBeNull();
expect(isDate(result.date)).toBe(true);
expect(Array.isArray(result.uniqueStringArray)).toBe(true);
expect(Array.isArray(result.uniqueStringArrayWithDefaultValue)).toBe(true);
expect(result.uniqueStringArrayWithDefaultValue[0]).toBe('test');
});

it('should exclude all unknown fields from the input data.', () => {
const data = {
date: new Date(),
Expand All @@ -79,7 +92,7 @@ describe('snapshotConverterFunctions()', () => {
expect(x.b).not.toBeDefined();
expect(x.c).not.toBeDefined();

expect(Object.keys(x).length).toBe(2);
expect(Object.keys(x).length).toBe(3);
});
});

Expand Down
2 changes: 2 additions & 0 deletions packages/firebase/test/src/lib/common/firestore.mock.item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const mockItemPrivateIdentity = firestoreModelIdentity(mockItemIdentity,
*/
export interface MockItemPrivate {
comments?: Maybe<string>;
values: string[];
createdAt: Date;
}

Expand All @@ -134,6 +135,7 @@ export const mockItemPrivateIdentifier = '0';
export const mockItemPrivateConverter = snapshotConverterFunctions({
fieldConversions: modelFieldConversions<MockItemPrivate, MockItemPrivateData>({
comments: optionalFirestoreString(),
values: firestoreUniqueStringArray(),
createdAt: firestoreDate({ saveDefaultAsNow: true })
})
});
Expand Down
50 changes: 46 additions & 4 deletions packages/firebase/test/src/lib/common/test.driver.accessor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { firstValueFrom } from 'rxjs';
import { SubscriptionObject } from '@dereekb/rxjs';
import { Transaction, DocumentReference, WriteBatch, FirestoreDocumentAccessor, makeDocuments, FirestoreDocumentDataAccessor, FirestoreContext, FirestoreDocument, RunTransaction, FirebaseAuthUserId } from '@dereekb/firebase';
import { MockItemDocument, MockItem, MockItemPrivateDocument, MockItemPrivateFirestoreCollection, MockItemPrivate, MockItemSubItem, MockItemSubItemDocument, MockItemSubItemFirestoreCollection, MockItemSubItemFirestoreCollectionGroup, MockItemUserFirestoreCollection, MockItemUserDocument, MockItemUser } from './firestore.mock.item';
import { Transaction, DocumentReference, WriteBatch, FirestoreDocumentAccessor, makeDocuments, FirestoreDocumentDataAccessor, FirestoreContext, FirestoreDocument, RunTransaction, FirebaseAuthUserId, DocumentSnapshot, FirestoreDataConverter } from '@dereekb/firebase';
import { MockItemDocument, MockItem, MockItemPrivateDocument, MockItemPrivateFirestoreCollection, MockItemPrivate, MockItemSubItem, MockItemSubItemDocument, MockItemSubItemFirestoreCollection, MockItemSubItemFirestoreCollectionGroup, MockItemUserFirestoreCollection, MockItemUserDocument, MockItemUser, mockItemConverter } from './firestore.mock.item';
import { MockItemCollectionFixture } from './firestore.mock.item.fixture';

/**
Expand Down Expand Up @@ -96,6 +96,48 @@ export function describeAccessorDriverTests(f: MockItemCollectionFixture) {
privateSub.destroy();
});

describe('get()', () => {
it('should read that data using the configured converter', async () => {
await itemPrivateDataDocument.accessor.set({ values: null } as any);
const dataWithoutConverter = (await itemPrivateDataDocument.accessor.getWithConverter(null)).data();

expect(dataWithoutConverter).toBeDefined();
expect(dataWithoutConverter!.values).toBeNull();

expect(itemPrivateDataDocument.documentRef.converter).toBeDefined();

const data = await itemPrivateDataDocument.snapshotData();
expect(data?.values).toBeDefined();
expect(data?.values).not.toBeNull(); // should not be null due to the snapshot converter config
});
});

describe('getWithConverter()', () => {
it('should get the results with the input converter', async () => {
await itemPrivateDataDocument.accessor.set({ values: null } as any);

const data = await itemPrivateDataDocument.snapshotData();
expect(data?.values).toBeDefined();

const dataWithoutConverter = (await itemPrivateDataDocument.accessor.getWithConverter(null)).data();

expect(dataWithoutConverter).toBeDefined();
expect(dataWithoutConverter!.values).toBeNull();
});

it('should get the results with the input converter with a type', async () => {
await itemPrivateDataDocument.accessor.set({ values: null } as any);

const data = await itemPrivateDataDocument.snapshotData();
expect(data?.values).toBeDefined();

const converter: FirestoreDataConverter<MockItem> = mockItemConverter;
const dataWithoutConverter: DocumentSnapshot<MockItem> = await itemPrivateDataDocument.accessor.getWithConverter(converter);

expect(dataWithoutConverter).toBeDefined();
});
});

describe('createOrUpdate()', () => {
it('should create the item if it does not exist', async () => {
let exists = await itemPrivateDataDocument.accessor.exists();
Expand All @@ -113,7 +155,7 @@ export function describeAccessorDriverTests(f: MockItemCollectionFixture) {
let exists = await privateDataAccessor.exists();
expect(exists).toBe(false);

await privateDataAccessor.set({ createdAt: new Date() });
await privateDataAccessor.set({ values: [], createdAt: new Date() });

exists = await privateDataAccessor.exists();
expect(exists).toBe(true);
Expand All @@ -122,7 +164,7 @@ export function describeAccessorDriverTests(f: MockItemCollectionFixture) {

describe('with item', () => {
beforeEach(async () => {
await privateDataAccessor.set({ createdAt: new Date() });
await privateDataAccessor.set({ values: [], createdAt: new Date() });
});

describe('accessor', () => {
Expand Down

0 comments on commit aef4b27

Please sign in to comment.