diff --git a/.prettierrc b/.prettierrc index 5ccac64c3..eec7c9704 100644 --- a/.prettierrc +++ b/.prettierrc @@ -10,5 +10,5 @@ "tabWidth": 2, "htmlWhitespaceSensitivity": "ignore", "endOfLine": "lf", - "printWidth": 400 + "printWidth": 500 } diff --git a/components/demo-firebase/src/lib/collection.ts b/components/demo-firebase/src/lib/collection.ts index c0955309a..492796b8c 100644 --- a/components/demo-firebase/src/lib/collection.ts +++ b/components/demo-firebase/src/lib/collection.ts @@ -1,9 +1,10 @@ import { FirestoreContext } from '@dereekb/firebase'; -import { guestbookEntryFirestoreCollectionFactory, GuestbookEntryFirestoreCollectionFactory, guestbookFirestoreCollection, GuestbookFirestoreCollection, GuestbookFirestoreCollections } from './guestbook'; +import { guestbookEntryFirestoreCollectionFactory, GuestbookEntryFirestoreCollectionFactory, guestbookEntryFirestoreCollectionGroup, GuestbookEntryFirestoreCollectionGroup, guestbookFirestoreCollection, GuestbookFirestoreCollection, GuestbookFirestoreCollections } from './guestbook'; import { profileFirestoreCollection, ProfileFirestoreCollection, ProfileFirestoreCollections, profilePrivateDataFirestoreCollectionFactory, ProfilePrivateDataFirestoreCollectionFactory } from './profile/profile'; export abstract class DemoFirestoreCollections implements ProfileFirestoreCollections, GuestbookFirestoreCollections { abstract readonly guestbookFirestoreCollection: GuestbookFirestoreCollection; + abstract readonly guestbookEntryCollectionGroup: GuestbookEntryFirestoreCollectionGroup; abstract readonly guestbookEntryCollectionFactory: GuestbookEntryFirestoreCollectionFactory; abstract readonly profileFirestoreCollection: ProfileFirestoreCollection; abstract readonly profilePrivateDataCollectionFactory: ProfilePrivateDataFirestoreCollectionFactory; @@ -12,6 +13,7 @@ export abstract class DemoFirestoreCollections implements ProfileFirestoreCollec export function makeDemoFirestoreCollections(firestoreContext: FirestoreContext): DemoFirestoreCollections { return { guestbookFirestoreCollection: guestbookFirestoreCollection(firestoreContext), + guestbookEntryCollectionGroup: guestbookEntryFirestoreCollectionGroup(firestoreContext), guestbookEntryCollectionFactory: guestbookEntryFirestoreCollectionFactory(firestoreContext), profileFirestoreCollection: profileFirestoreCollection(firestoreContext), profilePrivateDataCollectionFactory: profilePrivateDataFirestoreCollectionFactory(firestoreContext) diff --git a/components/demo-firebase/src/lib/guestbook/guestbook.ts b/components/demo-firebase/src/lib/guestbook/guestbook.ts index 933a9e5ba..269c7b833 100644 --- a/components/demo-firebase/src/lib/guestbook/guestbook.ts +++ b/components/demo-firebase/src/lib/guestbook/guestbook.ts @@ -1,4 +1,22 @@ -import { CollectionReference, AbstractFirestoreDocument, snapshotConverterFunctions, firestoreString, firestoreDate, FirestoreCollection, UserRelatedById, DocumentReferenceRef, FirestoreContext, FirestoreCollectionWithParent, firestoreBoolean, DocumentDataWithId, AbstractFirestoreDocumentWithParent, optionalFirestoreDate } from '@dereekb/firebase'; +import { + CollectionReference, + AbstractFirestoreDocument, + snapshotConverterFunctions, + firestoreString, + firestoreDate, + FirestoreCollection, + UserRelatedById, + DocumentReferenceRef, + FirestoreContext, + FirestoreCollectionWithParent, + firestoreBoolean, + DocumentDataWithId, + AbstractFirestoreDocumentWithParent, + optionalFirestoreDate, + DocumentReference, + FirestoreCollectionGroup, + CollectionGroup +} from '@dereekb/firebase'; import { Maybe } from '@dereekb/util'; export interface GuestbookFirestoreCollections { @@ -122,3 +140,18 @@ export function guestbookEntryFirestoreCollectionFactory(firestoreContext: Fires }); }; } + +export function guestbookEntryCollectionReference(context: FirestoreContext): CollectionGroup { + return context.collectionGroup(guestbookCollectionGuestbookEntryCollectionPath).withConverter(guestbookEntryConverter); +} + +export type GuestbookEntryFirestoreCollectionGroup = FirestoreCollectionGroup; + +export function guestbookEntryFirestoreCollectionGroup(firestoreContext: FirestoreContext): GuestbookEntryFirestoreCollectionGroup { + return firestoreContext.firestoreCollectionGroup({ + itemsPerPage: 50, + queryLike: guestbookEntryCollectionReference(firestoreContext), + makeDocument: (accessor, documentAccessor) => new GuestbookEntryDocument(undefined, accessor, documentAccessor), + firestoreContext + }); +} diff --git a/packages/firebase-server/src/lib/firestore/driver.accessor.ts b/packages/firebase-server/src/lib/firestore/driver.accessor.ts index cc338f0a9..5d7a4a6cf 100644 --- a/packages/firebase-server/src/lib/firestore/driver.accessor.ts +++ b/packages/firebase-server/src/lib/firestore/driver.accessor.ts @@ -1,6 +1,6 @@ import { FirestoreAccessorDriver, CollectionReference, Firestore, TransactionFunction, DocumentReference, TransactionFirestoreDocumentContextFactory, WriteBatchFirestoreDocumentContextFactory } from '@dereekb/firebase'; import { batch } from '@dereekb/util'; -import { CollectionReference as GoogleCloudCollectionReference, DocumentReference as GoogleCloudDocumentReference, Firestore as GoogleCloudFirestore } from '@google-cloud/firestore'; +import { CollectionGroup, CollectionReference as GoogleCloudCollectionReference, DocumentReference as GoogleCloudDocumentReference, Firestore as GoogleCloudFirestore } from '@google-cloud/firestore'; import { writeBatchDocumentContext } from './driver.accessor.batch'; import { defaultFirestoreDocumentContext } from './driver.accessor.default'; import { transactionDocumentContext } from './driver.accessor.transaction'; @@ -53,6 +53,7 @@ export function docRefForPath(start: DocRefForPathInput, path?: string, pathS export function firestoreClientAccessorDriver(): FirestoreAccessorDriver { return { doc: (collection: CollectionReference, path?: string, ...pathSegments: string[]) => docRefForPath(collection as GoogleCloudCollectionReference, path, pathSegments) as DocumentReference, + collectionGroup: (firestore: Firestore, collectionId: string) => (firestore as GoogleCloudFirestore).collectionGroup(collectionId) as CollectionGroup, collection: (firestore: Firestore, path: string, ...pathSegments: string[]) => collectionRefForPath(firestore as GoogleCloudFirestore, path, pathSegments) as CollectionReference, subcollection: (document: DocumentReference, path: string, ...pathSegments: string[]) => collectionRefForPath(document as GoogleCloudDocumentReference, path, pathSegments) as CollectionReference, transactionFactoryForFirestore: diff --git a/packages/firebase/src/lib/client/firestore/driver.accessor.ts b/packages/firebase/src/lib/client/firestore/driver.accessor.ts index 9b8058c52..bd0e7c064 100644 --- a/packages/firebase/src/lib/client/firestore/driver.accessor.ts +++ b/packages/firebase/src/lib/client/firestore/driver.accessor.ts @@ -1,7 +1,7 @@ import { Firestore, runTransaction } from '@firebase/firestore'; -import { doc, collection, writeBatch, Transaction } from 'firebase/firestore'; +import { doc, collection, writeBatch, Transaction, collectionGroup } from 'firebase/firestore'; import { FirestoreAccessorDriver } from '../../common/firestore/driver/accessor'; -import { FirestoreAccessorDriverCollectionRefFunction, FirestoreAccessorDriverDocumentRefFunction, FirestoreAccessorDriverSubcollectionRefFunction, TransactionFunction } from '../../common/firestore/driver'; +import { FirestoreAccessorDriverCollectionGroupFunction, FirestoreAccessorDriverCollectionRefFunction, FirestoreAccessorDriverDocumentRefFunction, FirestoreAccessorDriverSubcollectionRefFunction, TransactionFunction } from '../../common/firestore/driver'; import { writeBatchDocumentContext } from './driver.accessor.batch'; import { defaultFirestoreDocumentContext } from './driver.accessor.default'; import { transactionDocumentContext } from './driver.accessor.transaction'; @@ -11,6 +11,7 @@ import { WriteBatchFirestoreDocumentContextFactory } from '../../common/firestor export function firestoreClientAccessorDriver(): FirestoreAccessorDriver { return { doc: doc as unknown as FirestoreAccessorDriverDocumentRefFunction, + collectionGroup: collectionGroup as unknown as FirestoreAccessorDriverCollectionGroupFunction, collection: collection as unknown as FirestoreAccessorDriverCollectionRefFunction, subcollection: collection as unknown as FirestoreAccessorDriverSubcollectionRefFunction, transactionFactoryForFirestore: diff --git a/packages/firebase/src/lib/common/firestore/accessor/document.ts b/packages/firebase/src/lib/common/firestore/accessor/document.ts index 714b86d5d..dbe464bbb 100644 --- a/packages/firebase/src/lib/common/firestore/accessor/document.ts +++ b/packages/firebase/src/lib/common/firestore/accessor/document.ts @@ -7,6 +7,7 @@ import { DocumentReference, CollectionReference, Transaction, WriteBatch, Docume import { createOrUpdateWithAccessorSet, dataFromSnapshotStream, FirestoreDocumentDataAccessor } from './accessor'; import { CollectionReferenceRef, DocumentReferenceRef } from '../reference'; import { FirestoreDocumentContext } from './context'; +import { build, Maybe } from '@dereekb/util'; export interface FirestoreDocument = FirestoreDocumentDataAccessor> extends DocumentReferenceRef, CollectionReferenceRef { readonly accessor: A; @@ -16,11 +17,11 @@ export interface FirestoreDocument /** * Abstract FirestoreDocument implementation that extends a FirestoreDocumentDataAccessor. */ -export abstract class AbstractFirestoreDocument, A extends FirestoreDocumentDataAccessor = FirestoreDocumentDataAccessor> implements FirestoreDocument, FirestoreDocumentAccessorRef, CollectionReferenceRef { +export abstract class AbstractFirestoreDocument, A extends FirestoreDocumentDataAccessor = FirestoreDocumentDataAccessor> implements FirestoreDocument, LimitedFirestoreDocumentAccessorRef, CollectionReferenceRef { readonly stream$ = this.accessor.stream(); readonly data$: Observable = dataFromSnapshotStream(this.stream$); - constructor(readonly accessor: A, readonly documentAccessor: FirestoreDocumentAccessor) {} + constructor(readonly accessor: A, readonly documentAccessor: LimitedFirestoreDocumentAccessor) {} get id(): string { return this.documentRef.id; @@ -31,7 +32,8 @@ export abstract class AbstractFirestoreDocument { - return this.documentAccessor.collection; + // TODO: Only works if the documentRef has access to the parent ref + return this.accessor.documentRef.parent as CollectionReference; } snapshot(): Promise> { @@ -47,24 +49,14 @@ export abstract class AbstractFirestoreDocument = FirestoreDocument> { - readonly documentAccessor: FirestoreDocumentAccessor; +export interface LimitedFirestoreDocumentAccessorRef = FirestoreDocument, A extends LimitedFirestoreDocumentAccessor = LimitedFirestoreDocumentAccessor> { + readonly documentAccessor: A; } -export interface FirestoreDocumentAccessor = FirestoreDocument> extends CollectionReferenceRef, FirestoreAccessorDriverRef { - readonly databaseContext: FirestoreDocumentContext; - - /** - * Creates a new document. - */ - newDocument(): D; +export type FirestoreDocumentAccessorRef = FirestoreDocument> = LimitedFirestoreDocumentAccessorRef>; - /** - * Loads a document from the datastore with the given id/path. - * - * @param ref - */ - loadDocumentForPath(path: string, ...pathSegments: string[]): D; +export interface LimitedFirestoreDocumentAccessor = FirestoreDocument> extends FirestoreAccessorDriverRef { + readonly databaseContext: FirestoreDocumentContext; /** * Loads a document from the datastore. @@ -79,6 +71,22 @@ export interface FirestoreDocumentAccessor = F * @param document */ loadDocumentFrom(document: FirestoreDocument): D; +} + +export interface FirestoreDocumentAccessor = FirestoreDocument> extends LimitedFirestoreDocumentAccessor, CollectionReferenceRef, FirestoreAccessorDriverRef { + readonly databaseContext: FirestoreDocumentContext; + + /** + * Creates a new document. + */ + newDocument(): D; + + /** + * Loads a document from the datastore with the given id/path. + * + * @param ref + */ + loadDocumentForPath(path: string, ...pathSegments: string[]): D; /** * @@ -91,38 +99,37 @@ export interface FirestoreDocumentAccessor = F /** * Used to generate a FirestoreDocument from an input FirestoreDocumentDataAccessor instance. */ -export type FirestoreDocumentFactoryFunction = FirestoreDocument> = (accessor: FirestoreDocumentDataAccessor, documentAccessor: FirestoreDocumentAccessor) => D; +export type FirestoreDocumentFactoryFunction = FirestoreDocument> = (accessor: FirestoreDocumentDataAccessor, documentAccessor: LimitedFirestoreDocumentAccessor) => D; -// MARK: FirestoreDocumentAccessor +// MARK: LimitedFirestoreDocumentAccessor /** * Factory function used for creating a FirestoreDocumentAccessor. */ -export type FirestoreDocumentAccessorFactoryFunction = FirestoreDocument> = (context?: FirestoreDocumentContext) => FirestoreDocumentAccessor; +export type LimitedFirestoreDocumentAccessorFactoryFunction = FirestoreDocument, A extends LimitedFirestoreDocumentAccessor = LimitedFirestoreDocumentAccessor> = (context?: FirestoreDocumentContext) => A; /** * Factory type used for creating a FirestoreDocumentAccessor. */ -export interface FirestoreDocumentAccessorFactory = FirestoreDocument> { +export interface LimitedFirestoreDocumentAccessorFactory = FirestoreDocument, A extends LimitedFirestoreDocumentAccessor = LimitedFirestoreDocumentAccessor> { /** * Creates a new FirestoreDocumentAccessor using the given context. * * @param context Optional context to retrieve items from. */ - readonly documentAccessor: FirestoreDocumentAccessorFactoryFunction; + readonly documentAccessor: LimitedFirestoreDocumentAccessorFactoryFunction; } /** * FirestoreDocumentAccessor configuration. */ -export interface FirestoreDocumentAccessorFactoryConfig = FirestoreDocument> extends CollectionReferenceRef, FirestoreAccessorDriverRef { +export interface LimitedFirestoreDocumentAccessorFactoryConfig = FirestoreDocument> extends FirestoreAccessorDriverRef { readonly makeDocument: FirestoreDocumentFactoryFunction; } -export function firestoreDocumentAccessorFactory = FirestoreDocument>(config: FirestoreDocumentAccessorFactoryConfig): FirestoreDocumentAccessorFactoryFunction { - const { firestoreAccessorDriver, collection } = config; +export function limitedFirestoreDocumentAccessorFactory = FirestoreDocument>(config: LimitedFirestoreDocumentAccessorFactoryConfig): LimitedFirestoreDocumentAccessorFactoryFunction { + const { firestoreAccessorDriver } = config; return (context?: FirestoreDocumentContext) => { const databaseContext: FirestoreDocumentContext = context ?? config.firestoreAccessorDriver.defaultContextFactory(); - const dataAccessorFactory = databaseContext.accessorFactory; function loadDocument(ref: DocumentReference) { @@ -130,64 +137,109 @@ export function firestoreDocumentAccessorFactory { - return firestoreAccessorDriver.doc(collection, path, ...pathSegments); - } - - const documentAccessor: FirestoreDocumentAccessor = { - newDocument(): D { - const newDocRef = firestoreAccessorDriver.doc(collection); - return this.loadDocument(newDocRef); - }, - loadDocumentForPath(path: string, ...pathSegments: string[]): D { - if (!path) { - throw new Error('Path was not provided to loadDocumentForPath(). Use newDocument() for generating an id.'); - } - - return this.loadDocument(documentRefForPath(path, ...pathSegments)); - }, + const documentAccessor: LimitedFirestoreDocumentAccessor = { loadDocumentFrom(document: FirestoreDocument): D { return loadDocument(document.documentRef); }, loadDocument, - documentRefForPath, firestoreAccessorDriver, - databaseContext, - collection + databaseContext }; return documentAccessor; }; } +// MARK: FirestoreDocumentAccessor +/** + * Factory function used for creating a FirestoreDocumentAccessor. + */ +export type FirestoreDocumentAccessorFactoryFunction = FirestoreDocument> = LimitedFirestoreDocumentAccessorFactoryFunction>; + +/** + * Factory type used for creating a FirestoreDocumentAccessor. + */ +export type FirestoreDocumentAccessorFactory = FirestoreDocument> = LimitedFirestoreDocumentAccessorFactory>; + +/** + * FirestoreDocumentAccessor configuration. + */ +export interface FirestoreDocumentAccessorFactoryConfig = FirestoreDocument> extends CollectionReferenceRef, FirestoreAccessorDriverRef { + readonly makeDocument: FirestoreDocumentFactoryFunction; +} + +export function firestoreDocumentAccessorFactory = FirestoreDocument>(config: FirestoreDocumentAccessorFactoryConfig): FirestoreDocumentAccessorFactoryFunction { + const { firestoreAccessorDriver, collection } = config; + const limitedFirestoreDocumentAccessor = limitedFirestoreDocumentAccessorFactory(config); + + function documentRefForPath(path: string, ...pathSegments: string[]): DocumentReference { + return firestoreAccessorDriver.doc(collection, path, ...pathSegments); + } + + return (context?: FirestoreDocumentContext) => { + const documentAccessor: FirestoreDocumentAccessor = build>({ + base: limitedFirestoreDocumentAccessor(context), + build: (x) => { + x.collection = collection; + + x.newDocument = (): D => { + const newDocRef = firestoreAccessorDriver.doc(collection); + return documentAccessor.loadDocument(newDocRef); + }; + + x.documentRefForPath = documentRefForPath; + + x.loadDocumentForPath = (path: string, ...pathSegments: string[]): D => { + if (!path) { + throw new Error('Path was not provided to loadDocumentForPath(). Use newDocument() for generating an id.'); + } + + return documentAccessor.loadDocument(documentRefForPath(path, ...pathSegments)); + }; + } + }); + + return documentAccessor; + }; +} + // MARK: Extension -export interface FirestoreDocumentAccessorForTransactionFactory = FirestoreDocument> { +export interface LimitedFirestoreDocumentAccessorForTransactionFactory = FirestoreDocument, A extends LimitedFirestoreDocumentAccessor = LimitedFirestoreDocumentAccessor> { /** * Creates a new FirestoreDocumentAccessor for a Transaction. */ - documentAccessorForTransaction(transaction: Transaction): FirestoreDocumentAccessor; + documentAccessorForTransaction(transaction: Transaction): A; } +export type FirestoreDocumentAccessorForTransactionFactory = FirestoreDocument> = LimitedFirestoreDocumentAccessorForTransactionFactory>; -export interface FirestoreDocumentAccessorForWriteBatchFactory = FirestoreDocument> { +export interface LimitedFirestoreDocumentAccessorForWriteBatchFactory = FirestoreDocument, A extends LimitedFirestoreDocumentAccessor = LimitedFirestoreDocumentAccessor> { /** * Creates a new FirestoreDocumentAccessor for a WriteBatch. */ - documentAccessorForWriteBatch(writeBatch: WriteBatch): FirestoreDocumentAccessor; + documentAccessorForWriteBatch(writeBatch: WriteBatch): A; } +export type FirestoreDocumentAccessorForWriteBatchFactory = FirestoreDocument> = LimitedFirestoreDocumentAccessorForWriteBatchFactory>; -export interface FirestoreDocumentAccessorContextExtensionConfig = FirestoreDocument> extends FirestoreAccessorDriverRef { +export interface LimitedFirestoreDocumentAccessorContextExtensionConfig = FirestoreDocument> extends FirestoreAccessorDriverRef { + readonly documentAccessor: LimitedFirestoreDocumentAccessorFactoryFunction; +} + +export interface FirestoreDocumentAccessorContextExtensionConfig = FirestoreDocument> extends LimitedFirestoreDocumentAccessorContextExtensionConfig { readonly documentAccessor: FirestoreDocumentAccessorFactoryFunction; } +export interface LimitedFirestoreDocumentAccessorContextExtension = FirestoreDocument> extends LimitedFirestoreDocumentAccessorFactory, LimitedFirestoreDocumentAccessorForTransactionFactory, LimitedFirestoreDocumentAccessorForWriteBatchFactory {} export interface FirestoreDocumentAccessorContextExtension = FirestoreDocument> extends FirestoreDocumentAccessorFactory, FirestoreDocumentAccessorForTransactionFactory, FirestoreDocumentAccessorForWriteBatchFactory {} -export function firestoreDocumentAccessorContextExtension = FirestoreDocument>({ documentAccessor, firestoreAccessorDriver }: FirestoreDocumentAccessorContextExtensionConfig): FirestoreDocumentAccessorContextExtension { +export function firestoreDocumentAccessorContextExtension = FirestoreDocument>({ documentAccessor, firestoreAccessorDriver }: FirestoreDocumentAccessorContextExtensionConfig): FirestoreDocumentAccessorContextExtension; +export function firestoreDocumentAccessorContextExtension = FirestoreDocument>({ documentAccessor, firestoreAccessorDriver }: LimitedFirestoreDocumentAccessorContextExtensionConfig): LimitedFirestoreDocumentAccessorContextExtension; +export function firestoreDocumentAccessorContextExtension = FirestoreDocument>({ documentAccessor, firestoreAccessorDriver }: FirestoreDocumentAccessorContextExtensionConfig | LimitedFirestoreDocumentAccessorContextExtensionConfig) { return { documentAccessor, - documentAccessorForTransaction(transaction: Transaction): FirestoreDocumentAccessor { + documentAccessorForTransaction(transaction: Transaction) { return documentAccessor(firestoreAccessorDriver.transactionContextFactory(transaction)); }, - documentAccessorForWriteBatch(writeBatch: WriteBatch): FirestoreDocumentAccessor { + documentAccessorForWriteBatch(writeBatch: WriteBatch) { return documentAccessor(firestoreAccessorDriver.writeBatchContextFactory(writeBatch)); } }; @@ -199,8 +251,15 @@ export interface FirestoreDocumentWithParent, A extends FirestoreDocumentDataAccessor = FirestoreDocumentDataAccessor> extends AbstractFirestoreDocument implements FirestoreDocumentWithParent { - constructor(readonly parent: DocumentReference

, accessor: A, documentAccessor: FirestoreDocumentAccessor) { + private _parent: DocumentReference

; + + get parent() { + return this._parent; + } + + constructor(parent: Maybe>, accessor: A, documentAccessor: LimitedFirestoreDocumentAccessor) { super(accessor, documentAccessor); + this._parent = parent ?? ((accessor.documentRef.parent as CollectionReference).parent as DocumentReference

); } } diff --git a/packages/firebase/src/lib/common/firestore/accessor/document.utility.ts b/packages/firebase/src/lib/common/firestore/accessor/document.utility.ts index 78390e59b..92742934c 100644 --- a/packages/firebase/src/lib/common/firestore/accessor/document.utility.ts +++ b/packages/firebase/src/lib/common/firestore/accessor/document.utility.ts @@ -1,6 +1,6 @@ import { makeArray, Maybe, performMakeLoop, PromiseUtility } from '@dereekb/util'; import { DocumentDataWithId, DocumentReference, DocumentSnapshot, QuerySnapshot, Transaction } from '../types'; -import { FirestoreDocument, FirestoreDocumentAccessor, FirestoreDocumentAccessorContextExtension } from './document'; +import { FirestoreDocument, FirestoreDocumentAccessor, LimitedFirestoreDocumentAccessor, LimitedFirestoreDocumentAccessorContextExtension } from './document'; export function newDocuments>(documentAccessor: FirestoreDocumentAccessor, count: number): D[] { return makeArray({ count, make: () => documentAccessor.newDocument() }); @@ -43,15 +43,15 @@ export function getDocumentSnapshots>(document return PromiseUtility.runTasksForValues(documents, (x) => x.accessor.get()); } -export function loadDocumentsForSnapshots>(accessor: FirestoreDocumentAccessor, snapshots: QuerySnapshot): D[] { +export function loadDocumentsForSnapshots>(accessor: LimitedFirestoreDocumentAccessor, snapshots: QuerySnapshot): D[] { return snapshots.docs.map((x) => accessor.loadDocument(x.ref)); } -export function loadDocumentsForDocumentReferences>(accessor: FirestoreDocumentAccessor, refs: DocumentReference[]): D[] { +export function loadDocumentsForDocumentReferences>(accessor: LimitedFirestoreDocumentAccessor, refs: DocumentReference[]): D[] { return refs.map((x) => accessor.loadDocument(x)); } -export function loadDocumentsForValues>(accessor: FirestoreDocumentAccessor, values: I[], getRef: (value: I) => DocumentReference): D[] { +export function loadDocumentsForValues>(accessor: LimitedFirestoreDocumentAccessor, values: I[], getRef: (value: I) => DocumentReference): D[] { return values.map((x) => accessor.loadDocument(getRef(x))); } @@ -66,7 +66,7 @@ export type FirestoreDocumentLoader> = (refere * @param accessorContext * @returns */ -export function firestoreDocumentLoader>(accessorContext: FirestoreDocumentAccessorContextExtension): FirestoreDocumentLoader { +export function firestoreDocumentLoader>(accessorContext: LimitedFirestoreDocumentAccessorContextExtension): FirestoreDocumentLoader { return (references: DocumentReference[], transaction?: Transaction) => { const accessor = transaction ? accessorContext.documentAccessorForTransaction(transaction) : accessorContext.documentAccessor(); return loadDocumentsForDocumentReferences(accessor, references); diff --git a/packages/firebase/src/lib/common/firestore/collection/collection.group.ts b/packages/firebase/src/lib/common/firestore/collection/collection.group.ts new file mode 100644 index 000000000..02c4e7952 --- /dev/null +++ b/packages/firebase/src/lib/common/firestore/collection/collection.group.ts @@ -0,0 +1,51 @@ +import { FirestoreDocument, firestoreDocumentAccessorContextExtension, LimitedFirestoreDocumentAccessorFactoryConfig, limitedFirestoreDocumentAccessorFactory, LimitedFirestoreDocumentAccessorFactoryFunction } from '../accessor/document'; +import { FirestoreItemPageIterationBaseConfig, firestoreItemPageIterationFactory, FirestoreItemPageIterationFactoryFunction } from '../query/iterator'; +import { FirestoreContextReference } from '../reference'; +import { firestoreQueryFactory, FirestoreQueryFactory } from '../query/query'; +import { FirestoreDrivers } from '../driver/driver'; +import { firestoreCollectionQueryFactory } from './collection.query'; +import { FirestoreCollectionLike } from './collection'; + +/** + * FirestoreCollection configuration + */ +export interface FirestoreCollectionGroupConfig = FirestoreDocument> extends FirestoreContextReference, FirestoreDrivers, FirestoreItemPageIterationBaseConfig, LimitedFirestoreDocumentAccessorFactoryConfig {} + +/** + * Instance that provides several accessors for accessing documents of a collection. + */ +export interface FirestoreCollectionGroup = FirestoreDocument> extends FirestoreCollectionLike { + readonly config: FirestoreCollectionGroupConfig; +} + +/** + * Creates a new FirestoreCollection from the input config. + */ +export function makeFirestoreCollectionGroup>(config: FirestoreCollectionGroupConfig): FirestoreCollectionGroup { + const { queryLike, firestoreContext, firestoreAccessorDriver } = config; + const firestoreIteration: FirestoreItemPageIterationFactoryFunction = firestoreItemPageIterationFactory(config); + const documentAccessor: LimitedFirestoreDocumentAccessorFactoryFunction = limitedFirestoreDocumentAccessorFactory(config); + const queryFactory: FirestoreQueryFactory = firestoreQueryFactory(config); + + const documentAccessorExtension = firestoreDocumentAccessorContextExtension({ documentAccessor, firestoreAccessorDriver }); + const { queryDocument } = firestoreCollectionQueryFactory(queryFactory, documentAccessorExtension); + const { query } = queryFactory; + + return { + config, + queryLike, + firestoreContext, + ...documentAccessorExtension, + firestoreIteration, + query, + queryDocument + }; +} + +// CollectionGroup does not have a CollectionReference, and this MIGHT be a problem, although I don't believe the CollectionReferenceRef types really expose the collection that much anywayss, +// and instead just use the Query more. Will need to refactor a bit to get closer. Also, FirestoreCollectionGroup will not have a DocumentAccessor, probably, as they are only for querying... + +// so the option is either to refactor many types to work just off of Query instead of the CollectionReferenceRef, which might be fine, or the other is to make CollectionGroup a new specific type. +// It might end up being a mixture of the two as well. For instance, in dbx-firebase it may require using any found items to redirect them to their final area... + +// alternatively, we also avoid considering the use of CollectionGroups, as their usage is strange anyways. diff --git a/packages/firebase/src/lib/common/firestore/collection/collection.query.ts b/packages/firebase/src/lib/common/firestore/collection/collection.query.ts index 3d3d3e7c8..85a1adb2d 100644 --- a/packages/firebase/src/lib/common/firestore/collection/collection.query.ts +++ b/packages/firebase/src/lib/common/firestore/collection/collection.query.ts @@ -1,4 +1,4 @@ -import { FirestoreDocumentAccessorContextExtension } from './../accessor/document'; +import { FirestoreDocumentAccessorContextExtension, LimitedFirestoreDocumentAccessorContextExtension } from './../accessor/document'; import { ArrayOrValue, Maybe } from '@dereekb/util'; import { FirestoreDocument } from '../accessor/document'; import { documentReferencesFromSnapshot, FirestoreExecutableQuery, FirestoreQueryFactory } from '../query'; @@ -38,7 +38,7 @@ export interface FirestoreCollectionQueryFactory; } -export function firestoreCollectionQueryFactory>(queryFactory: FirestoreQueryFactory, accessorContext: FirestoreDocumentAccessorContextExtension): FirestoreCollectionQueryFactory { +export function firestoreCollectionQueryFactory>(queryFactory: FirestoreQueryFactory, accessorContext: LimitedFirestoreDocumentAccessorContextExtension): FirestoreCollectionQueryFactory { const documentLoader = firestoreDocumentLoader(accessorContext); const wrapQuery: (baseQuery: FirestoreExecutableQuery) => FirestoreCollectionExecutableDocumentQuery = (baseQuery: FirestoreExecutableQuery) => { diff --git a/packages/firebase/src/lib/common/firestore/collection/collection.ts b/packages/firebase/src/lib/common/firestore/collection/collection.ts index 437692a1e..cfda468e5 100644 --- a/packages/firebase/src/lib/common/firestore/collection/collection.ts +++ b/packages/firebase/src/lib/common/firestore/collection/collection.ts @@ -1,27 +1,51 @@ -import { FirestoreDocument, FirestoreDocumentAccessorFactory, FirestoreDocumentAccessorFactoryFunction, FirestoreDocumentAccessorFactoryConfig, firestoreDocumentAccessorFactory, FirestoreDocumentAccessorForTransactionFactory, FirestoreDocumentAccessorForWriteBatchFactory, firestoreDocumentAccessorContextExtension } from '../accessor/document'; +import { CollectionReferenceRef, FirestoreContextReference, QueryLikeReferenceRef } from '../reference'; +import { + FirestoreDocument, + FirestoreDocumentAccessorFactory, + FirestoreDocumentAccessorFactoryFunction, + FirestoreDocumentAccessorFactoryConfig, + firestoreDocumentAccessorFactory, + FirestoreDocumentAccessorForTransactionFactory, + FirestoreDocumentAccessorForWriteBatchFactory, + firestoreDocumentAccessorContextExtension, + LimitedFirestoreDocumentAccessorFactory, + LimitedFirestoreDocumentAccessorForTransactionFactory, + LimitedFirestoreDocumentAccessorForWriteBatchFactory, + LimitedFirestoreDocumentAccessor, + FirestoreDocumentAccessor +} from '../accessor/document'; import { FirestoreItemPageIterationBaseConfig, FirestoreItemPageIterationFactory, firestoreItemPageIterationFactory, FirestoreItemPageIterationFactoryFunction } from '../query/iterator'; -import { CollectionReferenceRef, FirestoreContextReference } from '../reference'; import { firestoreQueryFactory, FirestoreQueryFactory } from '../query/query'; import { FirestoreDrivers } from '../driver/driver'; import { FirestoreCollectionQueryFactory, firestoreCollectionQueryFactory } from './collection.query'; +import { build, Building } from '@dereekb/util'; +// MARK: FirestoreCollectionLike +/** + * Instance that provides several accessors for accessing documents of a collection. + */ +export interface FirestoreCollectionLike = FirestoreDocument, A extends LimitedFirestoreDocumentAccessor = LimitedFirestoreDocumentAccessor> + extends FirestoreContextReference, + QueryLikeReferenceRef, + FirestoreItemPageIterationFactory, + FirestoreQueryFactory, + LimitedFirestoreDocumentAccessorFactory, + LimitedFirestoreDocumentAccessorForTransactionFactory, + LimitedFirestoreDocumentAccessorForWriteBatchFactory, + FirestoreCollectionQueryFactory {} + +// MARK: FirestoreCollection /** * FirestoreCollection configuration */ -export interface FirestoreCollectionConfig = FirestoreDocument> extends FirestoreContextReference, FirestoreDrivers, FirestoreItemPageIterationBaseConfig, FirestoreDocumentAccessorFactoryConfig {} +export interface FirestoreCollectionConfig = FirestoreDocument> extends FirestoreContextReference, FirestoreDrivers, Omit, 'queryLike'>, Partial>, FirestoreDocumentAccessorFactoryConfig {} /** * Instance that provides several accessors for accessing documents of a collection. + * + * Provides a full FirestoreDocumentAccessor instead of limited accessors. */ -export interface FirestoreCollection = FirestoreDocument> - extends FirestoreContextReference, - CollectionReferenceRef, - FirestoreItemPageIterationFactory, - FirestoreDocumentAccessorFactory, - FirestoreQueryFactory, - FirestoreDocumentAccessorForTransactionFactory, - FirestoreDocumentAccessorForWriteBatchFactory, - FirestoreCollectionQueryFactory { +export interface FirestoreCollection = FirestoreDocument> extends FirestoreCollectionLike>, CollectionReferenceRef, FirestoreDocumentAccessorFactory, FirestoreDocumentAccessorForTransactionFactory, FirestoreDocumentAccessorForWriteBatchFactory { readonly config: FirestoreCollectionConfig; } @@ -35,8 +59,12 @@ export interface FirestoreCollectionRef = Fire /** * Creates a new FirestoreCollection from the input config. */ -export function makeFirestoreCollection>(config: FirestoreCollectionConfig): FirestoreCollection { +export function makeFirestoreCollection>(inputConfig: FirestoreCollectionConfig): FirestoreCollection { + const config = inputConfig as FirestoreCollectionConfig & QueryLikeReferenceRef; + const { collection, firestoreContext, firestoreAccessorDriver } = config; + (config as unknown as Building>).queryLike = collection; + const firestoreIteration: FirestoreItemPageIterationFactoryFunction = firestoreItemPageIterationFactory(config); const documentAccessor: FirestoreDocumentAccessorFactoryFunction = firestoreDocumentAccessorFactory(config); const queryFactory: FirestoreQueryFactory = firestoreQueryFactory(config); @@ -48,6 +76,7 @@ export function makeFirestoreCollection>(confi return { config, collection, + queryLike: collection, firestoreContext, ...documentAccessorExtension, firestoreIteration, diff --git a/packages/firebase/src/lib/common/firestore/collection/index.ts b/packages/firebase/src/lib/common/firestore/collection/index.ts index f5a9c88df..49af67826 100644 --- a/packages/firebase/src/lib/common/firestore/collection/index.ts +++ b/packages/firebase/src/lib/common/firestore/collection/index.ts @@ -1,3 +1,4 @@ export * from './collection'; +export * from './collection.group'; export * from './subcollection'; export * from './subcollection.single'; diff --git a/packages/firebase/src/lib/common/firestore/collection/subcollection.ts b/packages/firebase/src/lib/common/firestore/collection/subcollection.ts index 4ca3ca254..80cd8ae10 100644 --- a/packages/firebase/src/lib/common/firestore/collection/subcollection.ts +++ b/packages/firebase/src/lib/common/firestore/collection/subcollection.ts @@ -30,5 +30,6 @@ export type FirestoreCollectionWithParentFactory = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument>(config: FirestoreCollectionWithParentConfig): FirestoreCollectionWithParent { const result = makeFirestoreCollection(config) as FirestoreCollection & { parent: PD }; result.parent = config.parent; + // todo: consider throwing an exception if parent is not provided. return result; } diff --git a/packages/firebase/src/lib/common/firestore/context.ts b/packages/firebase/src/lib/common/firestore/context.ts index 3a2e29763..4bade75e7 100644 --- a/packages/firebase/src/lib/common/firestore/context.ts +++ b/packages/firebase/src/lib/common/firestore/context.ts @@ -1,8 +1,9 @@ import { FirestoreDocument } from './accessor/document'; -import { makeFirestoreCollection, FirestoreCollection, FirestoreCollectionConfig, FirestoreCollectionWithParent, FirestoreCollectionWithParentConfig, makeFirestoreCollectionWithParent, SingleItemFirestoreCollection, makeSingleItemFirestoreCollection, SingleItemFirestoreCollectionConfig } from './collection'; +import { makeFirestoreCollection, FirestoreCollection, FirestoreCollectionConfig, FirestoreCollectionWithParent, FirestoreCollectionWithParentConfig, makeFirestoreCollectionWithParent, SingleItemFirestoreCollection, makeSingleItemFirestoreCollection, SingleItemFirestoreCollectionConfig, FirestoreCollectionGroup, makeFirestoreCollectionGroup } from './collection'; import { FirestoreDrivers } from './driver/driver'; import { WriteBatchFactoryReference, RunTransactionFactoryReference } from './driver'; -import { DocumentReference, CollectionReference, DocumentData, Firestore } from './types'; +import { DocumentReference, CollectionReference, DocumentData, Firestore, CollectionGroup } from './types'; +import { QueryLikeReferenceRef } from './reference'; /** * A @dereekb/firestore FirestoreContext. Wraps the main Firestore context and the drivers, as well as utility/convenience functions. @@ -11,16 +12,21 @@ export interface FirestoreContext extends RunTr readonly firestore: F; readonly drivers: FirestoreDrivers; collection(path: string, ...pathSegments: string[]): CollectionReference; + collectionGroup(collectionId: string): CollectionGroup; subcollection(parent: DocumentReference, path: string, ...pathSegments: string[]): CollectionReference; firestoreCollection>(config: FirestoreContextFirestoreCollectionConfig): FirestoreCollection; + firestoreCollectionGroup>(config: FirestoreContextFirestoreCollectionGroupConfig): FirestoreCollectionGroup; firestoreCollectionWithParent = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument>(config: FirestoreContextFirestoreCollectionWithParentConfig): FirestoreCollectionWithParent; singleItemFirestoreCollection = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument>(config: FirestoreContextSingleItemFirestoreCollectionConfig): SingleItemFirestoreCollection; } export type FirestoreContextFirestoreCollectionConfig> = Omit, 'driverIdentifier' | 'driverType' | 'firestoreQueryDriver' | 'firestoreAccessorDriver'>; -export interface FirestoreContextFirestoreCollectionWithParentConfig = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument> extends FirestoreContextFirestoreCollectionConfig { +export type FirestoreContextFirestoreCollectionGroupConfig> = Omit, 'collection'> & QueryLikeReferenceRef; + +export interface FirestoreContextFirestoreCollectionWithParentConfig = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument> extends Omit, 'queryLike'> { readonly parent: PD; } + export interface FirestoreContextSingleItemFirestoreCollectionConfig = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument> extends FirestoreContextFirestoreCollectionWithParentConfig { readonly singleItemIdentifier: string; } @@ -38,8 +44,11 @@ export type FirestoreContextFactory = (firestor */ export function firestoreContextFactory(drivers: FirestoreDrivers): FirestoreContextFactory { return (firestore: F) => { - const makeFirestoreCollectionConfig = = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument>(config: FirestoreContextFirestoreCollectionConfig | FirestoreContextFirestoreCollectionWithParentConfig | FirestoreContextSingleItemFirestoreCollectionConfig) => ({ + const makeFirestoreCollectionConfig = = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument>( + config: FirestoreContextFirestoreCollectionConfig | FirestoreContextFirestoreCollectionGroupConfig | FirestoreContextFirestoreCollectionWithParentConfig | FirestoreContextSingleItemFirestoreCollectionConfig + ) => ({ ...config, + queryLike: (config as FirestoreContextFirestoreCollectionConfig).collection ?? (config as FirestoreContextFirestoreCollectionGroupConfig).queryLike, firestoreContext: context, driverIdentifier: drivers.driverIdentifier, driverType: drivers.driverType, @@ -47,16 +56,19 @@ export function firestoreContextFactory(drivers firestoreAccessorDriver: drivers.firestoreAccessorDriver }); - const firestoreCollection = >(config: FirestoreContextFirestoreCollectionConfig) => makeFirestoreCollection(makeFirestoreCollectionConfig(config)); + const firestoreCollection = >(config: FirestoreContextFirestoreCollectionConfig) => makeFirestoreCollection(makeFirestoreCollectionConfig(config) as FirestoreCollectionConfig); + const firestoreCollectionGroup = >(config: FirestoreContextFirestoreCollectionGroupConfig) => makeFirestoreCollectionGroup(makeFirestoreCollectionConfig(config)); const context: FirestoreContext = { firestore, drivers, + collectionGroup: (collectionId: string) => drivers.firestoreAccessorDriver.collectionGroup(firestore, collectionId), collection: (path: string, ...pathSegments: string[]) => drivers.firestoreAccessorDriver.collection(firestore, path, ...pathSegments), subcollection: drivers.firestoreAccessorDriver.subcollection, runTransaction: drivers.firestoreAccessorDriver.transactionFactoryForFirestore(firestore), batch: drivers.firestoreAccessorDriver.writeBatchFactoryForFirestore(firestore), firestoreCollection, + firestoreCollectionGroup, firestoreCollectionWithParent = FirestoreDocument, PD extends FirestoreDocument = FirestoreDocument>(inputConfig: FirestoreCollectionWithParentConfig): FirestoreCollectionWithParent { const config: FirestoreCollectionWithParentConfig = makeFirestoreCollectionConfig(inputConfig) as FirestoreCollectionWithParentConfig; return makeFirestoreCollectionWithParent(config); diff --git a/packages/firebase/src/lib/common/firestore/driver/accessor.ts b/packages/firebase/src/lib/common/firestore/driver/accessor.ts index 099a7594b..e84dbcd9b 100644 --- a/packages/firebase/src/lib/common/firestore/driver/accessor.ts +++ b/packages/firebase/src/lib/common/firestore/driver/accessor.ts @@ -1,10 +1,11 @@ -import { DocumentData, CollectionReference, DocumentReference, Firestore } from '../types'; +import { DocumentData, CollectionReference, CollectionGroup, DocumentReference, Firestore } from '../types'; import { DefaultFirestoreDocumentContextFactory } from '../accessor/context.default'; import { WriteBatchFirestoreDocumentContextFactory } from '../accessor/context.batch'; import { TransactionFirestoreDocumentContextFactory } from '../accessor/context.transaction'; import { FirestoreWriteBatchFactoryDriver } from './batch'; import { FirestoreTransactionFactoryDriver } from './transaction'; +export type FirestoreAccessorDriverCollectionGroupFunction = (firestore: Firestore, collectionId: string) => CollectionGroup; export type FirestoreAccessorDriverCollectionRefFunction = (firestore: Firestore, path: string, ...pathSegments: string[]) => CollectionReference; export type FirestoreAccessorDriverSubcollectionRefFunction = (document: DocumentReference, path: string, ...pathSegments: string[]) => CollectionReference; export type FirestoreAccessorDriverDocumentRefFunction = (collection: CollectionReference, path?: string, ...pathSegments: string[]) => DocumentReference; @@ -14,6 +15,7 @@ export type FirestoreAccessorDriverDocumentRefFunction = (coll */ export interface FirestoreAccessorDriver extends FirestoreTransactionFactoryDriver, FirestoreWriteBatchFactoryDriver { readonly doc: FirestoreAccessorDriverDocumentRefFunction; + readonly collectionGroup: FirestoreAccessorDriverCollectionGroupFunction; readonly collection: FirestoreAccessorDriverCollectionRefFunction; readonly subcollection: FirestoreAccessorDriverSubcollectionRefFunction; readonly defaultContextFactory: DefaultFirestoreDocumentContextFactory; diff --git a/packages/firebase/src/lib/common/firestore/query/iterator.ts b/packages/firebase/src/lib/common/firestore/query/iterator.ts index c9c3090da..233737bc0 100644 --- a/packages/firebase/src/lib/common/firestore/query/iterator.ts +++ b/packages/firebase/src/lib/common/firestore/query/iterator.ts @@ -2,9 +2,9 @@ import { PageLoadingState, ItemPageIterator, ItemPageIterationInstance, ItemPage import { QueryDocumentSnapshotArray, QuerySnapshot, SnapshotListenOptions } from '../types'; import { asArray, Maybe, lastValue, mergeIntoArray, ArrayOrValue } from '@dereekb/util'; import { from, Observable, of, exhaustMap } from 'rxjs'; -import { CollectionReferenceRef } from '../reference'; import { FirestoreQueryDriverRef } from '../driver/query'; import { FIRESTORE_LIMIT_QUERY_CONSTRAINT_TYPE, FirestoreQueryConstraint, limit, startAfter } from './constraint'; +import { QueryLikeReferenceRef } from '../reference'; export interface FirestoreItemPageIteratorFilter extends ItemPageLimit { /** @@ -17,7 +17,7 @@ export interface FirestoreItemPageIteratorFilter extends ItemPageLimit { constraints?: Maybe>; } -export interface FirestoreItemPageIterationBaseConfig extends CollectionReferenceRef, FirestoreQueryDriverRef, ItemPageLimit { +export interface FirestoreItemPageIterationBaseConfig extends QueryLikeReferenceRef, FirestoreQueryDriverRef, ItemPageLimit { itemsPerPage: number; } @@ -64,7 +64,7 @@ export function makeFirestoreItemPageIteratorDelegate(): FirestoreItemPageIte const { page, iteratorConfig } = request; const prevQueryResult$: Observable>> = page > 0 ? request.lastItem$ : of(undefined); - const { collection, itemsPerPage, filter, firestoreQueryDriver: driver } = iteratorConfig; + const { queryLike, itemsPerPage, filter, firestoreQueryDriver: driver } = iteratorConfig; const { limit: filterLimit, constraints: filterConstraints } = filter ?? {}; return prevQueryResult$.pipe( @@ -94,7 +94,7 @@ export function makeFirestoreItemPageIteratorDelegate(): FirestoreItemPageIte const constraintsWithLimit = [...constraints, limitConstraint]; // make query - const batchQuery = driver.query(collection, ...constraintsWithLimit); + const batchQuery = driver.query(queryLike, ...constraintsWithLimit); const resultPromise: Promise>> = driver.getDocs(batchQuery).then((snapshot) => { const time = new Date(); const docs = snapshot.docs; @@ -162,7 +162,7 @@ export type FirestoreItemPageIterationFactoryFunction = (filter?: FirestoreIt export function firestoreItemPageIterationFactory(baseConfig: FirestoreItemPageIterationBaseConfig): FirestoreItemPageIterationFactoryFunction { return (filter?: FirestoreItemPageIteratorFilter) => { const result: FirestoreItemPageIterationInstance = firestoreItemPageIteration({ - collection: baseConfig.collection, + queryLike: baseConfig.queryLike, itemsPerPage: baseConfig.itemsPerPage, firestoreQueryDriver: baseConfig.firestoreQueryDriver, maxPageLoadLimit: filter?.maxPageLoadLimit ?? baseConfig.maxPageLoadLimit, diff --git a/packages/firebase/src/lib/common/firestore/query/query.ts b/packages/firebase/src/lib/common/firestore/query/query.ts index ff46a1a72..fba0fe95b 100644 --- a/packages/firebase/src/lib/common/firestore/query/query.ts +++ b/packages/firebase/src/lib/common/firestore/query/query.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs'; import { ArrayOrValue, flattenArrayOrValueArray, Maybe } from '@dereekb/util'; -import { CollectionReferenceRef } from '../reference'; +import { QueryLikeReferenceRef } from '../reference'; import { Query, QueryDocumentSnapshot, QuerySnapshot, Transaction } from '../types'; import { addOrReplaceLimitInConstraints, FirestoreQueryConstraint } from './constraint'; import { FirestoreQueryDriverRef } from '../driver/query'; @@ -43,7 +43,7 @@ export interface FirestoreQueryFactory { readonly query: FirestoreQueryFactoryFunction; } -export interface FirestoreQueryConfig extends FirestoreQueryDriverRef, CollectionReferenceRef {} +export interface FirestoreQueryConfig extends FirestoreQueryDriverRef, QueryLikeReferenceRef {} /** * Creates a FirestoreCollectionQuery. @@ -52,7 +52,7 @@ export interface FirestoreQueryConfig extends FirestoreQueryDriverRef, Collec * @returns */ export function firestoreQueryFactory(config: FirestoreQueryConfig): FirestoreQueryFactory { - const { collection, firestoreQueryDriver: driver } = config; + const { queryLike, firestoreQueryDriver: driver } = config; const { getDocs, streamDocs, query: makeQuery } = driver; const extendQuery = (inputQuery: Query, queryConstraints: ArrayOrValue[]) => { @@ -74,6 +74,6 @@ export function firestoreQueryFactory(config: FirestoreQueryConfig): Fires }; return { - query: (...queryConstraints: ArrayOrValue[]) => extendQuery(collection, queryConstraints) + query: (...queryConstraints: ArrayOrValue[]) => extendQuery(queryLike, queryConstraints) }; } diff --git a/packages/firebase/src/lib/common/firestore/reference.ts b/packages/firebase/src/lib/common/firestore/reference.ts index 21c0dfe5f..42161639f 100644 --- a/packages/firebase/src/lib/common/firestore/reference.ts +++ b/packages/firebase/src/lib/common/firestore/reference.ts @@ -1,5 +1,12 @@ import { FirestoreContext } from './context'; -import { CollectionReference, DocumentReference, Firestore } from './types'; +import { CollectionReference, DocumentReference, Firestore, Query } from './types'; + +/** + * Contains a reference to a Query. + */ +export interface QueryLikeReferenceRef { + readonly queryLike: Query; +} /** * Contains a reference to a CollectionReference. diff --git a/packages/firebase/src/lib/common/firestore/types.ts b/packages/firebase/src/lib/common/firestore/types.ts index a92dbb4d1..3f6462d7d 100644 --- a/packages/firebase/src/lib/common/firestore/types.ts +++ b/packages/firebase/src/lib/common/firestore/types.ts @@ -125,6 +125,13 @@ export interface CollectionReference extends Query { withConverter(converter: null): CollectionReference; } +// MARK: CollectionGroup +export interface CollectionGroup extends Query { + readonly type?: 'query'; + withConverter(converter: FirestoreDataConverter): CollectionGroup; + withConverter(converter: null): CollectionGroup; +} + // MARK: Batch export interface WriteBatch { /** diff --git a/packages/firebase/test/src/lib/common/firestore.mock.item.fixture.ts b/packages/firebase/test/src/lib/common/firestore.mock.item.fixture.ts index 0f83488fd..321f67e72 100644 --- a/packages/firebase/test/src/lib/common/firestore.mock.item.fixture.ts +++ b/packages/firebase/test/src/lib/common/firestore.mock.item.fixture.ts @@ -22,6 +22,10 @@ export class MockItemCollectionFixtureInstance { return this.collections.mockItemSubItem; } + get mockItemSubItemGroup() { + return this.collections.mockItemSubItemGroup; + } + constructor(readonly fixture: MockItemCollectionFixture) {} } 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 2d332e8f7..c72895dc3 100644 --- a/packages/firebase/test/src/lib/common/firestore.mock.item.ts +++ b/packages/firebase/test/src/lib/common/firestore.mock.item.ts @@ -1,5 +1,5 @@ import { Maybe, modelFieldConversions } from '@dereekb/util'; -import { CollectionReference, FirestoreCollection, FirestoreContext, AbstractFirestoreDocument, SingleItemFirestoreCollection, FirestoreCollectionWithParent, AbstractFirestoreDocumentWithParent, firestoreString, firestoreBoolean, ExpectedFirestoreModelData, optionalFirestoreString, firestoreDate, optionalFirestoreNumber, snapshotConverterFunctions, FirestoreModelData } from '@dereekb/firebase'; +import { CollectionReference, FirestoreCollection, FirestoreContext, AbstractFirestoreDocument, SingleItemFirestoreCollection, FirestoreCollectionWithParent, AbstractFirestoreDocumentWithParent, firestoreString, firestoreBoolean, ExpectedFirestoreModelData, optionalFirestoreString, firestoreDate, optionalFirestoreNumber, snapshotConverterFunctions, FirestoreModelData, CollectionGroup, FirestoreCollectionGroup } from '@dereekb/firebase'; // MARK: Mock Item /** @@ -185,17 +185,34 @@ export function mockItemSubItemFirestoreCollection(firestoreContext: FirestoreCo }; } +export function mockItemSubItemCollectionReference(context: FirestoreContext): CollectionGroup { + return context.collectionGroup(mockItemSubItemCollectionPath).withConverter(mockItemSubItemConverter); +} + +export type MockItemSubItemFirestoreCollectionGroup = FirestoreCollectionGroup; + +export function mockItemSubItemFirestoreCollectionGroup(firestoreContext: FirestoreContext): MockItemSubItemFirestoreCollectionGroup { + return firestoreContext.firestoreCollectionGroup({ + itemsPerPage: 50, + queryLike: mockItemSubItemCollectionReference(firestoreContext), + makeDocument: (accessor, documentAccessor) => new MockItemSubItemDocument(undefined, accessor, documentAccessor), + firestoreContext + }); +} + // MARK: Collection export abstract class MockItemCollections { abstract readonly mockItem: MockItemFirestoreCollection; abstract readonly mockItemPrivate: MockItemPrivateFirestoreCollectionFactory; abstract readonly mockItemSubItem: MockItemSubItemFirestoreCollectionFactory; + abstract readonly mockItemSubItemGroup: MockItemSubItemFirestoreCollectionGroup; } export function makeMockItemCollections(firestoreContext: FirestoreContext): MockItemCollections { return { mockItem: mockItemFirestoreCollection(firestoreContext), mockItemPrivate: mockItemPrivateFirestoreCollection(firestoreContext), - mockItemSubItem: mockItemSubItemFirestoreCollection(firestoreContext) + mockItemSubItem: mockItemSubItemFirestoreCollection(firestoreContext), + mockItemSubItemGroup: mockItemSubItemFirestoreCollectionGroup(firestoreContext) }; } diff --git a/packages/firebase/test/src/lib/common/firestore.ts b/packages/firebase/test/src/lib/common/firestore.ts index ed6e5bbfe..3013df170 100644 --- a/packages/firebase/test/src/lib/common/firestore.ts +++ b/packages/firebase/test/src/lib/common/firestore.ts @@ -1,7 +1,10 @@ -import { PromiseUtility } from '@dereekb/util'; -import { Firestore, FirestoreAccessorDriver, FirestoreContext, FirestoreDrivers } from '@dereekb/firebase'; +import { Maybe, PromiseUtility } from '@dereekb/util'; +import { DocumentReference, Firestore, FirestoreAccessorDriver, FirestoreContext, FirestoreDrivers } from '@dereekb/firebase'; // MARK: Test Accessor +/** + * Used to override/extend a FirestoreAccessorDriver to provide better isolation between tests. + */ export interface TestingFirestoreAccessorDriver extends FirestoreAccessorDriver { /** * Gets the fuzzed path names map. @@ -20,10 +23,10 @@ export function makeTestingFirestoreAccesorDriver(driver: FirestoreAccessorDrive let fuzzerKey = 0; const time = new Date().getTime(); const fuzzedMap = new Map(); - const collection = driver.collection; + const { collection, subcollection, collectionGroup } = driver; - const fuzzedCollectionName = (path: string) => { - let fuzzedPath: string = fuzzedMap.get(path)!; + const fuzzedPathForPath = (path: string) => { + let fuzzedPath: Maybe = fuzzedMap.get(path); if (!fuzzedPath) { const random = Math.ceil(Math.random() * 9999) % 9999; @@ -35,21 +38,34 @@ export function makeTestingFirestoreAccesorDriver(driver: FirestoreAccessorDrive }; const fuzzedCollection = (f: Firestore, path: string) => { - const fuzzedPath = fuzzedCollectionName(path); + const fuzzedPath = fuzzedPathForPath(path); return collection(f, fuzzedPath); }; + const fuzzedSubcollection = (document: DocumentReference, path: string, ...pathSegments: string[]) => { + const fuzzedPath = fuzzedPathForPath(path); + const fuzzedPathSegments = pathSegments.map((x) => fuzzedPathForPath(x)); + return subcollection(document, fuzzedPath, ...fuzzedPathSegments); + }; + + const fuzzedCollectionGroup = (f: Firestore, collectionId: string) => { + const fuzzedPath = fuzzedPathForPath(collectionId); + return collectionGroup(f, fuzzedPath); + }; + const initWithCollectionNames = (collectionPaths: string[]) => { - collectionPaths.forEach((x) => fuzzedCollectionName(x)); + collectionPaths.forEach((x) => fuzzedPathForPath(x)); return fuzzedMap; }; - const injectedDriver = { + const injectedDriver: TestingFirestoreAccessorDriver = { ...driver, collection: fuzzedCollection, + collectionGroup: fuzzedCollectionGroup, + subcollection: fuzzedSubcollection, getFuzzedCollectionsNameMap: () => fuzzedMap, initWithCollectionNames - } as any; + }; return injectedDriver; } diff --git a/packages/firebase/test/src/lib/common/test.driver.query.ts b/packages/firebase/test/src/lib/common/test.driver.query.ts index db65a57b9..49240efd7 100644 --- a/packages/firebase/test/src/lib/common/test.driver.query.ts +++ b/packages/firebase/test/src/lib/common/test.driver.query.ts @@ -1,7 +1,7 @@ import { SubscriptionObject } from '@dereekb/rxjs'; import { filter, first, from, skip } from 'rxjs'; import { limit, orderBy, startAfter, startAt, where, limitToLast, endAt, endBefore, makeDocuments, FirestoreQueryFactoryFunction } from '@dereekb/firebase'; -import { MockItemDocument, MockItem } from './firestore.mock.item'; +import { MockItemDocument, MockItem, MockItemSubItemDocument, MockItemSubItem } from './firestore.mock.item'; import { MockItemCollectionFixture } from './firestore.mock.item.fixture'; /** @@ -11,23 +11,157 @@ import { MockItemCollectionFixture } from './firestore.mock.item.fixture'; */ export function describeQueryDriverTests(f: MockItemCollectionFixture) { describe('FirestoreQueryDriver', () => { + const testDocumentCount = 5; + + let items: MockItemDocument[]; + + beforeEach(async () => { + items = await makeDocuments(f.instance.firestoreCollection.documentAccessor(), { + count: testDocumentCount, + init: (i) => { + return { + value: `${i}`, + test: true + }; + } + }); + }); + + describe('collection group', () => { + const subItemCountPerItem = 2; + const totalSubItemsCount = subItemCountPerItem * testDocumentCount; + + let parentA: MockItemDocument; + + let querySubItems: FirestoreQueryFactoryFunction; + + let allSubItems: MockItemSubItemDocument[]; + + beforeEach(async () => { + querySubItems = f.instance.mockItemSubItemGroup.query; + parentA = items[0]; + + const results = await Promise.all( + items.map((parent: MockItemDocument) => + makeDocuments(f.instance.mockItemSubItemCollection(parent).documentAccessor(), { + count: subItemCountPerItem, + init: (i) => { + return { + value: i + }; + } + }) + ) + ); + + allSubItems = results.flat(); + }); + + describe('query', () => { + it('should return sub items', async () => { + const result = await querySubItems().getDocs(); + expect(result.docs.length).toBe(totalSubItemsCount); + }); + + describe('constraints', () => { + describe('where', () => { + it('should return the documents matching the query.', async () => { + const value = 0; + + const result = await querySubItems(where('value', '==', value)).getDocs(); + expect(result.docs.length).toBe(testDocumentCount); + expect(result.docs[0].data().value).toBe(value); + }); + }); + }); + + describe('streamDocs()', () => { + let sub: SubscriptionObject; + + beforeEach(() => { + sub = new SubscriptionObject(); + }); + + afterEach(() => { + sub.destroy(); + }); + + it('should emit when the query results update (an item is added).', (done) => { + const itemsToAdd = 1; + + let addCompleted = false; + let addSeen = false; + + function tryComplete() { + if (addSeen && addCompleted) { + done(); + } + } + + sub.subscription = querySubItems() + .streamDocs() + .pipe(filter((x) => x.docs.length > allSubItems.length)) + .subscribe((results) => { + addSeen = true; + expect(results.docs.length).toBe(allSubItems.length + itemsToAdd); + tryComplete(); + }); + + // add one item + makeDocuments(f.instance.mockItemSubItemCollection(parentA).documentAccessor(), { + count: itemsToAdd, + init: (i) => { + return { + value: i + }; + } + }).then(() => { + addCompleted = true; + tryComplete(); + }); + }); + + it('should emit when the query results update (an item is removed).', (done) => { + const itemsToRemove = 1; + + let deleteCompleted = false; + let deleteSeen = false; + + function tryComplete() { + if (deleteSeen && deleteCompleted) { + done(); + } + } + + sub.subscription = querySubItems() + .streamDocs() + .pipe(filter((x) => x.docs.length < allSubItems.length)) + .subscribe((results) => { + deleteSeen = true; + expect(results.docs.length).toBe(allSubItems.length - itemsToRemove); + tryComplete(); + }); + + allSubItems[0].accessor.exists().then((exists) => { + expect(exists).toBe(true); + + // remove one item + return allSubItems[0].accessor.delete().then(() => { + deleteCompleted = true; + tryComplete(); + }); + }); + }); + }); + }); + }); + describe('query', () => { - const testDocumentCount = 5; let query: FirestoreQueryFactoryFunction; - let items: MockItemDocument[]; - + beforeEach(async () => { query = f.instance.firestoreCollection.query; - items = await makeDocuments(f.instance.firestoreCollection.documentAccessor(), { - count: testDocumentCount, - init: (i) => { - return { - value: `${i}`, - test: true - }; - } - }); }); describe('streamDocs()', () => { diff --git a/setup/setup-project.sh b/setup/setup-project.sh index 2e5addac1..bddcf7b77 100755 --- a/setup/setup-project.sh +++ b/setup/setup-project.sh @@ -287,7 +287,6 @@ git commit --no-verify -m "checkpoint: added Docker files and other utility file # add semver for semantic versioning, husky for pre-commit hooks, and pretty-quick for running prettier npm install -D @jscutlery/semver husky pretty-quick @commitlint/cli @commitlint/config-angular - curl https://raw.githubusercontent.com/dereekb/dbx-components/$SOURCE_BRANCH/.commitlintrc.json -o .commitlintrc.json mkdir .husky