From aef4b27dabfa926af098d5c1afac4fb77302b4ef Mon Sep 17 00:00:00 2001 From: Derek Burgman Date: Thu, 16 Jun 2022 23:38:58 -0500 Subject: [PATCH] feat: added getWithConverter() --- .../lib/firestore/driver.accessor.batch.ts | 6 ++- .../lib/firestore/driver.accessor.default.ts | 6 ++- .../firestore/driver.accessor.transaction.ts | 6 ++- .../client/firestore/driver.accessor.batch.ts | 27 +++------- .../firestore/driver.accessor.default.ts | 6 ++- .../firestore/driver.accessor.transaction.ts | 6 ++- .../lib/common/firestore/accessor/accessor.ts | 9 +++- .../firestore/accessor/accessor.wrap.ts | 7 ++- .../firestore/snapshot/snapshot.spec.ts | 17 ++++++- .../src/lib/common/firestore.mock.item.ts | 2 + .../src/lib/common/test.driver.accessor.ts | 50 +++++++++++++++++-- 11 files changed, 108 insertions(+), 34 deletions(-) diff --git a/packages/firebase-server/src/lib/firestore/driver.accessor.batch.ts b/packages/firebase-server/src/lib/firestore/driver.accessor.batch.ts index 6781aec89..37a2270e5 100644 --- a/packages/firebase-server/src/lib/firestore/driver.accessor.batch.ts +++ b/packages/firebase-server/src/lib/firestore/driver.accessor.batch.ts @@ -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 /** @@ -26,6 +26,10 @@ export class WriteBatchFirestoreDocumentDataAccessor implements FirestoreDocu return this.documentRef.get(); } + getWithConverter(converter: null | FirestoreDataConverter): Promise> { + return this.documentRef.withConverter(converter as FirestoreDataConverter).get(); + } + delete(params?: FirestoreDocumentDeleteParams): Promise { this.batch.delete(this.documentRef, params?.precondition); return Promise.resolve(); diff --git a/packages/firebase-server/src/lib/firestore/driver.accessor.default.ts b/packages/firebase-server/src/lib/firestore/driver.accessor.default.ts index 59d0dde5a..71f3e3bad 100644 --- a/packages/firebase-server/src/lib/firestore/driver.accessor.default.ts +++ b/packages/firebase-server/src/lib/firestore/driver.accessor.default.ts @@ -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 implements FirestoreDocumentDataAccessor { @@ -22,6 +22,10 @@ export class DefaultFirestoreDocumentDataAccessor implements FirestoreDocumen return this.documentRef.get(); } + getWithConverter(converter: null | FirestoreDataConverter): Promise> { + return this.documentRef.withConverter(converter as FirestoreDataConverter).get(); + } + delete(params: FirestoreDocumentDeleteParams): Promise { return this.documentRef.delete(params?.precondition); } diff --git a/packages/firebase-server/src/lib/firestore/driver.accessor.transaction.ts b/packages/firebase-server/src/lib/firestore/driver.accessor.transaction.ts index beec57e73..3c22ab911 100644 --- a/packages/firebase-server/src/lib/firestore/driver.accessor.transaction.ts +++ b/packages/firebase-server/src/lib/firestore/driver.accessor.transaction.ts @@ -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 /** @@ -26,6 +26,10 @@ export class TransactionFirestoreDocumentDataAccessor implements FirestoreDoc return this.transaction.get(this.documentRef); } + getWithConverter(converter: null | FirestoreDataConverter): Promise> { + return this.transaction.get(this.documentRef.withConverter(converter as FirestoreDataConverter)); + } + delete(): Promise { this.transaction.delete(this.documentRef); return Promise.resolve(); diff --git a/packages/firebase/src/lib/client/firestore/driver.accessor.batch.ts b/packages/firebase/src/lib/client/firestore/driver.accessor.batch.ts index c2141c585..f11f1ff0c 100644 --- a/packages/firebase/src/lib/client/firestore/driver.accessor.batch.ts +++ b/packages/firebase/src/lib/client/firestore/driver.accessor.batch.ts @@ -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 implements FirestoreDocumentDataAccessor { - constructor(readonly batch: FirebaseFirestoreWriteBatch, readonly documentRef: DocumentReference) {} - - stream(): Observable> { - return from(this.get()); - } - - create(data: WithFieldValue): Promise { - return createWithAccessor(this)(data) as Promise; - } - - exists(): Promise { - return this.get().then((x) => x.exists()); - } - - get(): Promise> { - return getDoc(this.documentRef); +export class WriteBatchFirestoreDocumentDataAccessor extends DefaultFirestoreDocumentDataAccessor implements FirestoreDocumentDataAccessor { + constructor(readonly batch: FirebaseFirestoreWriteBatch, documentRef: DocumentReference) { + super(documentRef); } - delete(): Promise { + override delete(): Promise { this.batch.delete(this.documentRef); return Promise.resolve(); } - set(data: WithFieldValue, options?: SetOptions): Promise { + override set(data: WithFieldValue, options?: SetOptions): Promise { this.batch.set(this.documentRef, data, options as SetOptions); return Promise.resolve(); } - update(data: UpdateData): Promise { + override update(data: UpdateData): Promise { this.batch.update(this.documentRef, data as FirestoreUpdateData); return Promise.resolve(); } diff --git a/packages/firebase/src/lib/client/firestore/driver.accessor.default.ts b/packages/firebase/src/lib/client/firestore/driver.accessor.default.ts index 6b9eee3d3..b8baac132 100644 --- a/packages/firebase/src/lib/client/firestore/driver.accessor.default.ts +++ b/packages/firebase/src/lib/client/firestore/driver.accessor.default.ts @@ -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 @@ -24,6 +24,10 @@ export class DefaultFirestoreDocumentDataAccessor implements FirestoreDocumen return getDoc(this.documentRef); } + getWithConverter(converter: null | FirestoreDataConverter): Promise> { + return getDoc(this.documentRef.withConverter(converter as FirestoreDataConverter)) as Promise>; + } + delete(): Promise { return deleteDoc(this.documentRef); } diff --git a/packages/firebase/src/lib/client/firestore/driver.accessor.transaction.ts b/packages/firebase/src/lib/client/firestore/driver.accessor.transaction.ts index 8c237bdca..cd866a379 100644 --- a/packages/firebase/src/lib/client/firestore/driver.accessor.transaction.ts +++ b/packages/firebase/src/lib/client/firestore/driver.accessor.transaction.ts @@ -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 @@ -26,6 +26,10 @@ export class TransactionFirestoreDocumentDataAccessor implements FirestoreDoc return this.transaction.get(this.documentRef); } + getWithConverter(converter: null | FirestoreDataConverter): Promise> { + return this.transaction.get(this.documentRef.withConverter(converter as FirestoreDataConverter)) as Promise>; + } + delete(): Promise { this.transaction.delete(this.documentRef); return Promise.resolve(); diff --git a/packages/firebase/src/lib/common/firestore/accessor/accessor.ts b/packages/firebase/src/lib/common/firestore/accessor/accessor.ts index a3ebfcbaf..b23b92b35 100644 --- a/packages/firebase/src/lib/common/firestore/accessor/accessor.ts +++ b/packages/firebase/src/lib/common/firestore/accessor/accessor.ts @@ -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'; @@ -28,6 +28,13 @@ export interface FirestoreDocumentDataAccessor extends Docu * Returns the current snapshot. */ get(): Promise>; + /** + * Gets the data from the datastore using the input converter. + * + * @param converter + */ + getWithConverter(converter: null): Promise>; + getWithConverter(converter: null | FirestoreDataConverter): Promise>; /** * Whether or not the target object currently exists. */ diff --git a/packages/firebase/src/lib/common/firestore/accessor/accessor.wrap.ts b/packages/firebase/src/lib/common/firestore/accessor/accessor.wrap.ts index 0b28a9dd0..765c20078 100644 --- a/packages/firebase/src/lib/common/firestore/accessor/accessor.wrap.ts +++ b/packages/firebase/src/lib/common/firestore/accessor/accessor.wrap.ts @@ -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 @@ -10,7 +10,6 @@ import { FirestoreDocumentDataAccessor, FirestoreDocumentDataAccessorFactory, Fi */ export abstract class AbstractFirestoreDocumentDataAccessorWrapper implements FirestoreDocumentDataAccessor { constructor(readonly accessor: FirestoreDocumentDataAccessor) {} - get documentRef(): DocumentReference { return this.accessor.documentRef; } @@ -27,6 +26,10 @@ export abstract class AbstractFirestoreDocumentDataAccessorWrapper(converter: null | FirestoreDataConverter): Promise> { + return this.accessor.getWithConverter(converter); + } + exists(): Promise { return this.accessor.exists(); } diff --git a/packages/firebase/src/lib/common/firestore/snapshot/snapshot.spec.ts b/packages/firebase/src/lib/common/firestore/snapshot/snapshot.spec.ts index bf5cec8ae..90f83ca61 100644 --- a/packages/firebase/src/lib/common/firestore/snapshot/snapshot.spec.ts +++ b/packages/firebase/src/lib/common/firestore/snapshot/snapshot.spec.ts @@ -7,12 +7,14 @@ import { firestoreBoolean, firestoreDate, firestoreString, firestoreUniqueString export interface TestSnapshotDefaults { date: Date; uniqueStringArray: string[]; + uniqueStringArrayWithDefaultValue: string[]; } export const testSnapshotDefaultsConverter = snapshotConverterFunctions({ fields: { date: firestoreDate({ saveDefaultAsNow: true }), - uniqueStringArray: firestoreUniqueStringArray() + uniqueStringArray: firestoreUniqueStringArray(), + uniqueStringArrayWithDefaultValue: firestoreUniqueStringArray({ default: () => ['test'] }) } }); @@ -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(), @@ -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); }); }); diff --git a/packages/firebase/test/src/lib/common/firestore.mock.item.ts b/packages/firebase/test/src/lib/common/firestore.mock.item.ts index 7dfeb4afb..e76527427 100644 --- a/packages/firebase/test/src/lib/common/firestore.mock.item.ts +++ b/packages/firebase/test/src/lib/common/firestore.mock.item.ts @@ -110,6 +110,7 @@ export const mockItemPrivateIdentity = firestoreModelIdentity(mockItemIdentity, */ export interface MockItemPrivate { comments?: Maybe; + values: string[]; createdAt: Date; } @@ -134,6 +135,7 @@ export const mockItemPrivateIdentifier = '0'; export const mockItemPrivateConverter = snapshotConverterFunctions({ fieldConversions: modelFieldConversions({ comments: optionalFirestoreString(), + values: firestoreUniqueStringArray(), createdAt: firestoreDate({ saveDefaultAsNow: true }) }) }); diff --git a/packages/firebase/test/src/lib/common/test.driver.accessor.ts b/packages/firebase/test/src/lib/common/test.driver.accessor.ts index 0dfa9c916..69fe9f1d4 100644 --- a/packages/firebase/test/src/lib/common/test.driver.accessor.ts +++ b/packages/firebase/test/src/lib/common/test.driver.accessor.ts @@ -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'; /** @@ -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 = mockItemConverter; + const dataWithoutConverter: DocumentSnapshot = 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(); @@ -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); @@ -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', () => {