diff --git a/addon/-private/flatten-doc-snapshot.ts b/addon/-private/flatten-doc-snapshot.ts index 89a9840a..17674cd2 100644 --- a/addon/-private/flatten-doc-snapshot.ts +++ b/addon/-private/flatten-doc-snapshot.ts @@ -1,9 +1,6 @@ import { DocumentSnapshot } from 'firebase/firestore'; -import firebase from 'firebase/compat/app'; -export default function flattenDocSnapshot( - docSnapshot: firebase.firestore.DocumentSnapshot | DocumentSnapshot, -): { +export default function flattenDocSnapshot(docSnapshot: DocumentSnapshot): { id: string, [key: string]: unknown, } { diff --git a/addon/-private/realtime-tracker.ts b/addon/-private/realtime-tracker.ts deleted file mode 100644 index a5a1385a..00000000 --- a/addon/-private/realtime-tracker.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - eslint - @typescript-eslint/no-empty-function: off, - ember/use-ember-data-rfc-395-imports: off -*/ - -import { next } from '@ember/runloop'; -import DS from 'ember-data'; -import Store from '@ember-data/store'; - -import firebase from 'firebase/compat/app'; - -import flattenDocSnapshotData from 'ember-cloud-firestore-adapter/-private/flatten-doc-snapshot'; - -interface Model { - [key: string]: { - record: { - [key: string]: { hasOnSnapshotRunAtLeastOnce: boolean }, - }, - meta: { hasOnSnapshotRunAtLeastOnce: boolean, hasTrackedAllRecords: boolean }, - }; -} - -interface Query { - [key: string]: { - hasOnSnapshotRunAtLeastOnce: boolean, - - unsubscribe(): void, - }; -} - -export default class RealtimeTracker { - private model: Model = {}; - - private query: Query = {}; - - private store: Store; - - constructor(store: Store) { - this.store = store; - } - - public trackFindRecordChanges( - modelName: string, - docRef: firebase.firestore.DocumentReference, - ): void { - const { id } = docRef; - - if (!this.isRecordTracked(modelName, id)) { - this.trackModel(modelName); - - this.model[modelName].record[id] = { hasOnSnapshotRunAtLeastOnce: false }; - - const unsubscribe = docRef.onSnapshot((docSnapshot) => { - if (this.model[modelName].record[id].hasOnSnapshotRunAtLeastOnce) { - if (docSnapshot.exists) { - const record = this.store.peekRecord(modelName, id); - - if (record && !record.isSaving) { - const flatRecord = flattenDocSnapshotData(docSnapshot); - const normalizedRecord = this.store.normalize(modelName, flatRecord); - - this.store.push(normalizedRecord); - } - } else { - unsubscribe(); - this.unloadRecord(modelName, id); - } - } else { - this.model[modelName].record[id].hasOnSnapshotRunAtLeastOnce = true; - } - }, (error) => { - const record = this.store.peekRecord(modelName, id); - - if (record) { - // When we lose permission to view the document, we unload it from the store. However, - // any template that has rendered the record will still be intact even if it no longer - // exists in the store. - // - // We set some properties here to give our templates the opportunity to react to this - // scenario. - record.set('isUnloaded', true); - record.set('unloadReason', error); - this.unloadRecord(modelName, id); - } - - delete this.model[modelName].record[id]; - }); - } - } - - public trackFindAllChanges( - modelName: string, - collectionRef: firebase.firestore.CollectionReference, - ): void { - if (!this.model[modelName]?.meta.hasTrackedAllRecords) { - this.trackModel(modelName); - - collectionRef.onSnapshot((querySnapshot) => { - if (this.model[modelName].meta.hasOnSnapshotRunAtLeastOnce) { - querySnapshot.forEach((docSnapshot) => ( - this.store.findRecord(modelName, docSnapshot.id, { - adapterOptions: { isRealtime: true }, - }) - )); - } else { - this.model[modelName].meta.hasOnSnapshotRunAtLeastOnce = true; - } - }, () => { - this.model[modelName].meta.hasTrackedAllRecords = false; - }); - - this.model[modelName].meta.hasTrackedAllRecords = true; - this.model[modelName].meta.hasOnSnapshotRunAtLeastOnce = false; - } - } - - public trackFindHasManyChanges( - modelName: string | number, - id: string, - field: string, - collectionRef: firebase.firestore.CollectionReference | firebase.firestore.Query, - ): void { - const queryId = `${modelName}_${id}_${field}`; - - if (!Object.prototype.hasOwnProperty.call(this.query, queryId)) { - this.query[queryId] = { - hasOnSnapshotRunAtLeastOnce: false, - unsubscribe: () => {}, - }; - } - - const unsubscribe = collectionRef.onSnapshot(() => { - if (this.query[queryId].hasOnSnapshotRunAtLeastOnce) { - // Schedule for next runloop to avoid race condition errors for when a record is unloaded - // in the find record tracker because it was deleted in the database. Basically, we should - // unload any deleted records first before refreshing the has-many array. - next(() => { - const hasManyRef = this.store.peekRecord(modelName, id).hasMany(field); - - hasManyRef.reload().then(() => this.query[queryId].unsubscribe()); - }); - } else { - this.query[queryId].hasOnSnapshotRunAtLeastOnce = true; - } - }, () => delete this.query[queryId]); - - this.query[queryId].unsubscribe = unsubscribe; - } - - public trackQueryChanges( - firestoreQuery: firebase.firestore.Query, - recordArray: DS.AdapterPopulatedRecordArray, - queryId?: string, - ): void { - const finalQueryId = queryId || Math.random().toString(32).slice(2).substring(0, 5); - - if (!Object.prototype.hasOwnProperty.call(this.query, finalQueryId)) { - this.query[finalQueryId] = { - hasOnSnapshotRunAtLeastOnce: false, - unsubscribe: () => {}, - }; - } - - const unsubscribe = firestoreQuery.onSnapshot(() => { - if (this.query[finalQueryId].hasOnSnapshotRunAtLeastOnce) { - // Schedule for next runloop to avoid race condition errors for when a record is unloaded - // in the find record tracker because it was deleted in the database. Basically, we should - // unload any deleted records first before refreshing the query array. - next(() => ( - recordArray.update().then(() => this.query[finalQueryId].unsubscribe()) - )); - } else { - this.query[finalQueryId].hasOnSnapshotRunAtLeastOnce = true; - } - }, () => delete this.query[finalQueryId]); - - this.query[finalQueryId].unsubscribe = unsubscribe; - } - - private isRecordTracked(modelName: string, id: string): boolean { - return this.model[modelName]?.record?.[id] !== undefined; - } - - private trackModel(type: string): void { - if (!Object.prototype.hasOwnProperty.call(this.model, type)) { - this.model[type] = { - meta: { - hasOnSnapshotRunAtLeastOnce: false, - hasTrackedAllRecords: false, - }, - record: {}, - }; - } - } - - private unloadRecord(modelName: string, id: string): void { - const record = this.store.peekRecord(modelName, id); - - if (record && !record.isSaving) { - this.store.unloadRecord(record); - } - - delete this.model[modelName].record[id]; - } -} diff --git a/addon/adapters/cloud-firestore-modular.ts b/addon/adapters/cloud-firestore-modular.ts index c707d488..022e44d9 100644 --- a/addon/adapters/cloud-firestore-modular.ts +++ b/addon/adapters/cloud-firestore-modular.ts @@ -14,22 +14,22 @@ import Store from '@ember-data/store'; import { CollectionReference, DocumentReference, + Firestore, Query, WriteBatch, } from 'firebase/firestore'; -import firebase from 'firebase/compat/app'; import { collection, doc, getDoc, getDocs, + getFirestore, query, where, writeBatch, } from 'ember-cloud-firestore-adapter/firebase/firestore'; import { AdapterRecordNotFoundError } from 'ember-cloud-firestore-adapter/utils/custom-errors'; -import FirebaseService from 'ember-cloud-firestore-adapter/services/-firebase'; import FirestoreDataManager from 'ember-cloud-firestore-adapter/services/-firestore-data-manager'; import buildCollectionName from 'ember-cloud-firestore-adapter/-private/build-collection-name'; import flattenDocSnapshot from 'ember-cloud-firestore-adapter/-private/flatten-doc-snapshot'; @@ -42,9 +42,9 @@ interface AdapterOption { isRealtime?: boolean; queryId?: string; - buildReference?(db: firebase.firestore.Firestore): CollectionReference; + buildReference?(db: Firestore): CollectionReference; filter?(db: CollectionReference): Query; - include?(batch: WriteBatch, db: firebase.firestore.Firestore): void; + include?(batch: WriteBatch, db: Firestore): void; [key: string]: unknown; } @@ -68,15 +68,12 @@ interface HasManyRelationshipMeta { options: { isRealtime?: boolean, - buildReference?(db: firebase.firestore.Firestore, record: unknown): CollectionReference, + buildReference?(db: Firestore, record: unknown): CollectionReference, filter?(db: CollectionReference | Query, record: unknown): Query, }; } export default class CloudFirestoreModularAdapter extends Adapter { - @service('-firebase') - protected declare firebase: FirebaseService; - @service('-firestore-data-manager') private declare firestoreDataManager: FirestoreDataManager; @@ -89,7 +86,7 @@ export default class CloudFirestoreModularAdapter extends Adapter { } public generateIdForRecord(_store: Store, type: string): string { - const db = this.firebase.firestore(); + const db = getFirestore(); const collectionName = buildCollectionName(type); return doc(collection(db, collectionName)).id; @@ -134,7 +131,7 @@ export default class CloudFirestoreModularAdapter extends Adapter { snapshot: Snapshot, ): RSVP.Promise { return new RSVP.Promise((resolve, reject) => { - const db = this.firebase.firestore(); + const db = getFirestore(); const collectionRef = this.buildCollectionRef(type.modelName, snapshot.adapterOptions); const docRef = doc(collectionRef, snapshot.id); const batch = writeBatch(db); @@ -183,7 +180,7 @@ export default class CloudFirestoreModularAdapter extends Adapter { ): RSVP.Promise { return new RSVP.Promise(async (resolve, reject) => { try { - const db = this.firebase.firestore(); + const db = getFirestore(); const colRef = collection(db, buildCollectionName(type.modelName)); const querySnapshot = snapshotRecordArray?.adapterOptions?.isRealtime && !this.isFastBoot ? await this.firestoreDataManager.findAllRealtime(type.modelName, colRef) @@ -241,7 +238,7 @@ export default class CloudFirestoreModularAdapter extends Adapter { urlNodes.pop(); - const db = this.firebase.firestore(); + const db = getFirestore(); const docRef = doc(db, urlNodes.join('/'), id); const modelName = relationship.type; const docSnapshot = relationship.options.isRealtime && !this.isFastBoot @@ -292,7 +289,7 @@ export default class CloudFirestoreModularAdapter extends Adapter { modelName: string, adapterOptions?: AdapterOption, ): CollectionReference { - const db = this.firebase.firestore(); + const db = getFirestore(); return adapterOptions?.buildReference?.(db) || collection(db, buildCollectionName(modelName)); } @@ -308,13 +305,13 @@ export default class CloudFirestoreModularAdapter extends Adapter { } private addIncludeToWriteBatch(batch: WriteBatch, adapterOptions?: AdapterOption): void { - const db = this.firebase.firestore(); + const db = getFirestore(); adapterOptions?.include?.(batch, db); } private buildWriteBatch(docRef: DocumentReference, snapshot: Snapshot): WriteBatch { - const db = this.firebase.firestore(); + const db = getFirestore(); const batch = writeBatch(db); this.addDocRefToWriteBatch(batch, docRef, snapshot); @@ -329,7 +326,7 @@ export default class CloudFirestoreModularAdapter extends Adapter { url: string, relationship: HasManyRelationshipMeta, ): CollectionReference | Query { - const db = this.firebase.firestore(); + const db = getFirestore(); if (relationship.options.buildReference) { const collectionRef = relationship.options.buildReference(db, snapshot.record); diff --git a/addon/adapters/cloud-firestore.ts b/addon/adapters/cloud-firestore.ts deleted file mode 100644 index 004feefd..00000000 --- a/addon/adapters/cloud-firestore.ts +++ /dev/null @@ -1,431 +0,0 @@ -/* - eslint - ember/use-ember-data-rfc-395-imports: off, - ember/no-ember-super-in-es-classes: off -*/ - -import { getOwner } from '@ember/application'; -import { inject as service } from '@ember/service'; -import Adapter from '@ember-data/adapter'; -import DS from 'ember-data'; -import RSVP from 'rsvp'; -import Store from '@ember-data/store'; - -import FirebaseService from 'ember-cloud-firestore-adapter/services/-firebase'; -import firebase from 'firebase/compat/app'; - -import { collection, doc } from 'ember-cloud-firestore-adapter/firebase/firestore'; -import { AdapterRecordNotFoundError } from 'ember-cloud-firestore-adapter/utils/custom-errors'; -import RealtimeTracker from 'ember-cloud-firestore-adapter/-private/realtime-tracker'; -import buildCollectionName from 'ember-cloud-firestore-adapter/-private/build-collection-name'; -import flattenDocSnapshot from 'ember-cloud-firestore-adapter/-private/flatten-doc-snapshot'; - -interface ModelClass { - modelName: string; -} - -interface AdapterOption { - isRealtime?: boolean; - queryId?: string; - - buildReference?(db: firebase.firestore.Firestore): firebase.firestore.CollectionReference; - filter?(db: firebase.firestore.CollectionReference): firebase.firestore.Query; - include?(batch: firebase.firestore.WriteBatch, db: firebase.firestore.Firestore): void; - - [key: string]: unknown; -} - -interface Snapshot extends DS.Snapshot { - adapterOptions: AdapterOption; -} - -interface SnapshotRecordArray extends DS.SnapshotRecordArray { - adapterOptions: AdapterOption; -} - -interface BelongsToRelationshipMeta { - type: string; - options: { isRealtime?: boolean }; -} - -interface HasManyRelationshipMeta { - key: string; - type: string; - options: { - isRealtime?: boolean, - - buildReference?( - db: firebase.firestore.Firestore, - record: unknown, - ): firebase.firestore.CollectionReference, - filter?( - db: firebase.firestore.CollectionReference | firebase.firestore.Query, - record: unknown, - ): firebase.firestore.Query, - }; -} - -export default class CloudFirestoreAdapter extends Adapter { - @service('-firebase') - declare protected firebase: FirebaseService; - - protected referenceKeyName = 'referenceTo'; - - declare private realtimeTracker: RealtimeTracker; - - private get isFastBoot(): boolean { - const fastboot = getOwner(this).lookup('service:fastboot'); - - return fastboot && fastboot.isFastBoot; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public constructor(...args: any[]) { - super(...args); - - this.realtimeTracker = new RealtimeTracker(getOwner(this).lookup('service:store')); - } - - public generateIdForRecord(_store: Store, type: string): string { - const db = this.firebase.firestore(); - const collectionName = buildCollectionName(type); - - return doc(collection(db, collectionName)).id; - } - - public createRecord( - store: Store, - type: ModelClass, - snapshot: Snapshot, - ): RSVP.Promise { - return this.updateRecord(store, type, snapshot); - } - - public updateRecord( - _store: Store, - type: ModelClass, - snapshot: Snapshot, - ): RSVP.Promise { - return new RSVP.Promise((resolve, reject) => { - const docRef = this.buildCollectionRef( - type.modelName, - snapshot.adapterOptions, - ).doc(snapshot.id); - const batch = this.buildWriteBatch(docRef, snapshot); - - batch.commit().then(() => { - const data = this.serialize(snapshot, { includeId: true }); - - resolve(data); - - if (snapshot.adapterOptions?.isRealtime && !this.isFastBoot) { - // Setup realtime listener for record - this.fetchRecord(type, snapshot.id, snapshot.adapterOptions); - } - }).catch((e) => { - reject(e); - }); - }); - } - - public deleteRecord( - _store: Store, - type: ModelClass, - snapshot: Snapshot, - ): RSVP.Promise { - return new RSVP.Promise((resolve, reject) => { - const db = this.firebase.firestore(); - const docRef = this.buildCollectionRef( - type.modelName, - snapshot.adapterOptions, - ).doc(snapshot.id); - const batch = db.batch(); - - batch.delete(docRef); - this.addIncludeToWriteBatch(batch, snapshot.adapterOptions); - - batch.commit().then(() => { - resolve(); - }).catch((e) => { - reject(e); - }); - }); - } - - public findRecord( - _store: Store, - type: ModelClass, - id: string, - snapshot: Snapshot, - ): RSVP.Promise { - return this.fetchRecord(type, id, snapshot.adapterOptions); - } - - public findAll( - _store: Store, - type: ModelClass, - _sinceToken: string, - snapshotRecordArray?: SnapshotRecordArray, - ): RSVP.Promise { - return new RSVP.Promise((resolve, reject) => { - const db = this.firebase.firestore(); - const collectionRef = db.collection(buildCollectionName(type.modelName)); - const unsubscribe = collectionRef.onSnapshot(async (querySnapshot) => { - if (snapshotRecordArray?.adapterOptions?.isRealtime && !this.isFastBoot) { - this.realtimeTracker?.trackFindAllChanges(type.modelName, collectionRef); - } - - const requests = querySnapshot.docs.map((docSnapshot) => ( - this.fetchRecord(type, docSnapshot.id, snapshotRecordArray?.adapterOptions) - )); - - try { - resolve(await RSVP.Promise.all(requests)); - } catch (error) { - reject(error); - } - - unsubscribe(); - }, (error) => reject(error)); - }); - } - - public findBelongsTo( - _store: Store, - _snapshot: Snapshot, - url: string, - relationship: BelongsToRelationshipMeta, - ): RSVP.Promise { - const type = { modelName: relationship.type }; - const urlNodes = url.split('/'); - const id = urlNodes[urlNodes.length - 1]; - - urlNodes.pop(); - - return this.fetchRecord(type, id, { - isRealtime: relationship.options.isRealtime, - - buildReference(db: firebase.firestore.Firestore) { - return db.collection(urlNodes.join('/')); - }, - }); - } - - public findHasMany( - store: Store, - snapshot: Snapshot, - url: string, - relationship: HasManyRelationshipMeta, - ): RSVP.Promise { - return new RSVP.Promise((resolve, reject) => { - const collectionRef = this.buildHasManyCollectionRef(store, snapshot, url, relationship); - const unsubscribe = collectionRef.onSnapshot(async (querySnapshot) => { - if (relationship.options.isRealtime && !this.isFastBoot) { - this.realtimeTracker?.trackFindHasManyChanges( - snapshot.modelName, - snapshot.id, - relationship.key, - collectionRef, - ); - } - - const requests = this.findHasManyRecords(relationship, querySnapshot); - - try { - resolve(await RSVP.Promise.all(requests)); - } catch (error) { - reject(error); - } - - unsubscribe(); - }, (error) => reject(error)); - }); - } - - public query( - _store: Store, - type: ModelClass, - query: AdapterOption, - recordArray: DS.AdapterPopulatedRecordArray, - ): RSVP.Promise { - return new RSVP.Promise((resolve, reject) => { - const collectionRef = this.buildCollectionRef(type.modelName, query); - const firestoreQuery = query.filter?.(collectionRef) || collectionRef - const unsubscribe = firestoreQuery.onSnapshot(async (querySnapshot) => { - if (query.isRealtime && !this.isFastBoot) { - this.realtimeTracker?.trackQueryChanges(firestoreQuery, recordArray, query.queryId); - } - - const requests = this.findQueryRecords(type, query, querySnapshot); - - try { - resolve(await RSVP.Promise.all(requests)); - } catch (error) { - reject(error); - } - - unsubscribe(); - }, (error) => reject(error)); - }); - } - - private buildCollectionRef( - modelName: string, - adapterOptions?: AdapterOption, - ): firebase.firestore.CollectionReference { - const db = this.firebase.firestore(); - - return adapterOptions?.buildReference?.(db) || db.collection(buildCollectionName(modelName)); - } - - private fetchRecord( - type: ModelClass, - id: string, - adapterOption?: AdapterOption, - ): RSVP.Promise { - return new RSVP.Promise((resolve, reject) => { - const docRef = this.buildCollectionRef(type.modelName, adapterOption).doc(id); - const unsubscribe = docRef.onSnapshot((docSnapshot) => { - if (docSnapshot.exists) { - if (adapterOption?.isRealtime && !this.isFastBoot) { - this.realtimeTracker?.trackFindRecordChanges(type.modelName, docRef); - } - - resolve(flattenDocSnapshot(docSnapshot)); - } else { - reject(new AdapterRecordNotFoundError(`Record ${id} for model type ${type.modelName} doesn't exist`)); - } - - unsubscribe(); - }, (error) => reject(error)); - }); - } - - private addDocRefToWriteBatch( - batch: firebase.firestore.WriteBatch, - docRef: firebase.firestore.DocumentReference, - snapshot: Snapshot, - ): void { - const data = this.serialize(snapshot, {}); - - batch.set(docRef, data, { merge: true }); - } - - private addIncludeToWriteBatch( - batch: firebase.firestore.WriteBatch, - adapterOptions?: AdapterOption, - ): void { - const db = this.firebase.firestore(); - - adapterOptions?.include?.(batch, db); - } - - private buildWriteBatch( - docRef: firebase.firestore.DocumentReference, - snapshot: Snapshot, - ): firebase.firestore.WriteBatch { - const db = this.firebase.firestore(); - const batch = db.batch(); - - this.addDocRefToWriteBatch(batch, docRef, snapshot); - this.addIncludeToWriteBatch(batch, snapshot.adapterOptions); - - return batch; - } - - private buildHasManyCollectionRef( - store: Store, - snapshot: Snapshot, - url: string, - relationship: HasManyRelationshipMeta, - ): firebase.firestore.CollectionReference | firebase.firestore.Query { - const db = this.firebase.firestore(); - - if (relationship.options.buildReference) { - const collectionRef = relationship.options.buildReference(db, snapshot.record); - - return relationship.options.filter?.(collectionRef, snapshot.record) || collectionRef; - } - - const cardinality = snapshot.type.determineRelationshipType(relationship, store); - - if (cardinality === 'manyToOne') { - const inverse = snapshot.type.inverseFor(relationship.key, store); - const snapshotCollectionName = buildCollectionName(snapshot.modelName.toString()); - const snapshotDocRef = db.doc(`${snapshotCollectionName}/${snapshot.id}`); - const collectionRef = db.collection(url).where(inverse.name, '==', snapshotDocRef); - - return relationship.options.filter?.(collectionRef, snapshot.record) || collectionRef; - } - - const collectionRef = db.collection(url); - - return relationship.options.filter?.(collectionRef, snapshot.record) || collectionRef; - } - - private findHasManyRecords( - relationship: HasManyRelationshipMeta, - querySnapshot: firebase.firestore.QuerySnapshot, - ): RSVP.Promise[] { - return querySnapshot.docs.map((docSnapshot) => { - const type = { modelName: relationship.type }; - const referenceTo = docSnapshot.get(this.referenceKeyName); - - if (referenceTo && referenceTo.firestore) { - return this.fetchRecord(type, referenceTo.id, { - isRealtime: relationship.options.isRealtime, - - buildReference(db: firebase.firestore.Firestore) { - return db.collection(referenceTo.parent.path); - }, - }); - } - - const adapterOptions = { - isRealtime: relationship.options.isRealtime, - - buildReference(db: firebase.firestore.Firestore) { - return db.collection(docSnapshot.ref.parent.path); - }, - }; - - return this.fetchRecord(type, docSnapshot.id, adapterOptions); - }); - } - - private findQueryRecords( - type: ModelClass, - option: AdapterOption, - querySnapshot: firebase.firestore.QuerySnapshot, - ): RSVP.Promise[] { - return querySnapshot.docs.map((docSnapshot) => { - const referenceTo = docSnapshot.get(this.referenceKeyName); - - if (referenceTo && referenceTo.firestore) { - const request = this.fetchRecord(type, referenceTo.id, { - isRealtime: option.isRealtime, - - buildReference() { - return referenceTo.parent; - }, - }); - - return request; - } - - return this.fetchRecord(type, docSnapshot.id, { - ...option, - - buildReference() { - return docSnapshot.ref.parent; - }, - }); - }); - } -} - -declare module 'ember-data/types/registries/adapter' { - export default interface AdapterRegistry { - 'cloud-firestore': CloudFirestoreAdapter; - } -} diff --git a/addon/authenticators/firebase.ts b/addon/authenticators/firebase.ts index 3beb66d9..ac6896d5 100644 --- a/addon/authenticators/firebase.ts +++ b/addon/authenticators/firebase.ts @@ -1,47 +1,42 @@ import { getOwner } from '@ember/application'; -import { inject as service } from '@ember/service'; -import { User, UserCredential } from 'firebase/auth'; +import { Auth, User, UserCredential } from 'firebase/auth'; import BaseAuthenticator from 'ember-simple-auth/authenticators/base'; -import firebase from 'firebase/compat/app'; import { + getAuth, getRedirectResult, onAuthStateChanged, signInWithCustomToken, signOut, } from 'ember-cloud-firestore-adapter/firebase/auth'; -import FirebaseService from 'ember-cloud-firestore-adapter/services/-firebase'; interface AuthenticateCallback { - (auth: firebase.auth.Auth): Promise; + (auth: Auth): Promise; } export default class FirebaseAuthenticator extends BaseAuthenticator { - @service('-firebase') - private firebase!: FirebaseService; - /* eslint-disable @typescript-eslint/no-explicit-any */ private get fastboot(): any { return getOwner(this).lookup('service:fastboot'); } public async authenticate(callback: AuthenticateCallback): Promise<{ user: User | null }> { - const auth = this.firebase.auth(); + const auth = getAuth(); const credential = await callback(auth); return { user: credential.user }; } public invalidate(): Promise { - const auth = this.firebase.auth(); + const auth = getAuth(); return signOut(auth); } public restore(): Promise<{ user: User | null }> { return new Promise((resolve, reject) => { - const auth = this.firebase.auth(); + const auth = getAuth(); if ( this.fastboot?.isFastBoot diff --git a/addon/instance-initializers/firebase-settings.ts b/addon/instance-initializers/firebase-settings.ts index 8e7492bd..b579f440 100644 --- a/addon/instance-initializers/firebase-settings.ts +++ b/addon/instance-initializers/firebase-settings.ts @@ -1,8 +1,14 @@ import ApplicationInstance from '@ember/application/instance'; -import firebase from 'firebase/compat/app'; +import { FirebaseApp, FirebaseOptions } from 'firebase/app'; +import { Firestore } from 'firebase/firestore'; + +import { initializeApp } from 'ember-cloud-firestore-adapter/firebase/app'; +import { connectFirestoreEmulator, getFirestore, initializeFirestore } from 'ember-cloud-firestore-adapter/firebase/firestore'; +import { connectAuthEmulator, getAuth } from 'ember-cloud-firestore-adapter/firebase/auth'; interface FirestoreAddonConfig { + isCustomSetup?: boolean; settings?: { [key: string]: string }; emulator?: { hostname: string, @@ -12,50 +18,64 @@ interface FirestoreAddonConfig { } interface AuthAddonConfig { + isCustomSetup?: boolean; emulator?: { hostname: string, port: number, + options?: { disableWarnings: boolean } }; } -function setupFirestore(app: firebase.app.App, config: FirestoreAddonConfig) { - const db = app.firestore(); +interface AddonConfig { + firebaseConfig: FirebaseOptions, + firestore: FirestoreAddonConfig; + auth: AuthAddonConfig; +} +function getDb(app: FirebaseApp, config: FirestoreAddonConfig): Firestore { if (config.settings) { - db.settings(config.settings); + return initializeFirestore(app, config.settings); } + return getFirestore(app); +} + +function setupFirestore(app: FirebaseApp, config: FirestoreAddonConfig): void { + const db = getDb(app, config); + if (config.emulator) { const { hostname, port, options } = config.emulator; - db.useEmulator(hostname, port, options); + connectFirestoreEmulator(db, hostname, port, options); } } -function setupAuth(app: firebase.app.App, config: AuthAddonConfig) { - const auth = app.auth(); - +function setupAuth(app: FirebaseApp, config: AuthAddonConfig) { if (config.emulator) { const { hostname, port } = config.emulator; - auth.useEmulator(`http://${hostname}:${port}`); + connectAuthEmulator(getAuth(app), `http://${hostname}:${port}`, config.emulator.options); + } +} + +function setupModularInstance(config: AddonConfig) { + const app = initializeApp(config.firebaseConfig); + + if (!config.firestore.isCustomSetup) { + setupFirestore(app, config.firestore); + } + + if (!config.auth.isCustomSetup) { + setupAuth(app, config.auth); } } export function initialize(appInstance: ApplicationInstance): void { const config = appInstance.resolveRegistration('config:environment'); - const addonConfig = config['ember-cloud-firestore-adapter']; + const addonConfig: AddonConfig = config['ember-cloud-firestore-adapter']; try { - const app: firebase.app.App = appInstance.lookup('service:-firebase'); - - if (addonConfig.firestore) { - setupFirestore(app, addonConfig.firestore); - } - - if (addonConfig.auth) { - setupAuth(app, addonConfig.auth); - } + setupModularInstance(addonConfig); } catch (e) { if (e.code !== 'failed-precondition') { throw new Error(`There was a problem with initializing Firebase. Check if you've configured the addon properly. | Error: ${e}`); diff --git a/addon/serializers/cloud-firestore-modular.ts b/addon/serializers/cloud-firestore-modular.ts index 0fde98fd..b959bc1b 100644 --- a/addon/serializers/cloud-firestore-modular.ts +++ b/addon/serializers/cloud-firestore-modular.ts @@ -5,17 +5,14 @@ no-param-reassign: off, */ -import { inject as service } from '@ember/service'; import { isNone } from '@ember/utils'; import DS from 'ember-data'; import JSONSerializer from '@ember-data/serializer/json'; import Store from '@ember-data/store'; -import { CollectionReference, DocumentReference } from 'firebase/firestore'; -import firebase from 'firebase/compat/app'; +import { CollectionReference, DocumentReference, Firestore } from 'firebase/firestore'; -import { doc } from 'ember-cloud-firestore-adapter/firebase/firestore'; -import FirebaseService from 'ember-cloud-firestore-adapter/services/-firebase'; +import { doc, getFirestore } from 'ember-cloud-firestore-adapter/firebase/firestore'; import buildCollectionName from 'ember-cloud-firestore-adapter/-private/build-collection-name'; interface Links { @@ -32,7 +29,7 @@ interface RelationshipDefinition { key: string; type: string; options: { - buildReference?(db: firebase.firestore.Firestore): CollectionReference + buildReference?(db: Firestore): CollectionReference }; } @@ -46,9 +43,6 @@ interface ModelClass { } export default class CloudFirestoreSerializer extends JSONSerializer { - @service('-firebase') - private firebase!: FirebaseService; - public extractRelationship( relationshipModelName: string, relationshipHash: DocumentReference, @@ -104,7 +98,7 @@ export default class CloudFirestoreSerializer extends JSONSerializer { super.serializeBelongsTo(snapshot, json, relationship); if (json[relationship.key]) { - const db = this.firebase.firestore(); + const db = getFirestore(); const docId = json[relationship.key] as string; if (relationship.options.buildReference) { diff --git a/addon/serializers/cloud-firestore.ts b/addon/serializers/cloud-firestore.ts deleted file mode 100644 index 9ee95a68..00000000 --- a/addon/serializers/cloud-firestore.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - eslint - @typescript-eslint/ban-types: off, - ember/use-ember-data-rfc-395-imports: off, - no-param-reassign: off, -*/ - -import { inject as service } from '@ember/service'; -import { isNone } from '@ember/utils'; -import DS from 'ember-data'; -import JSONSerializer from '@ember-data/serializer/json'; -import Store from '@ember-data/store'; - -import FirebaseService from 'ember-cloud-firestore-adapter/services/-firebase'; -import firebase from 'firebase/compat/app'; - -import buildCollectionName from 'ember-cloud-firestore-adapter/-private/build-collection-name'; - -interface Links { - [key: string]: string; -} - -interface ResourceHash { - id: string; - links: Links; - [key: string]: string | Links | firebase.firestore.CollectionReference; -} - -interface RelationshipDefinition { - key: string; - type: string; - options: { - buildReference?(db: firebase.firestore.Firestore): firebase.firestore.CollectionReference - }; -} - -interface ModelClass { - modelName: string; - determineRelationshipType(descriptor: { kind: string, type: string }, store: Store): string; - eachRelationship(callback: (name: string, descriptor: { - kind: string, - type: string, - }) => void): void; -} - -export default class CloudFirestoreSerializer extends JSONSerializer { - @service('-firebase') - private firebase!: FirebaseService; - - public extractRelationship( - relationshipModelName: string, - relationshipHash: firebase.firestore.DocumentReference, - ): { id: string, type: string } | {} { - if (isNone(relationshipHash)) { - return super.extractRelationship(relationshipModelName, relationshipHash); - } - - const pathNodes = relationshipHash.path.split('/'); - const belongsToId = pathNodes[pathNodes.length - 1]; - - return { id: belongsToId, type: relationshipModelName }; - } - - public extractRelationships(modelClass: ModelClass, resourceHash: ResourceHash): {} { - const newResourceHash = { ...resourceHash }; - const links: { [key: string]: string } = {}; - - modelClass.eachRelationship((name, descriptor) => { - if (descriptor.kind === 'belongsTo') { - if (resourceHash[name]) { - const data = resourceHash[name] as firebase.firestore.CollectionReference; - - links[name] = data.path; - } - } else { - const cardinality = modelClass.determineRelationshipType(descriptor, this.store); - let hasManyPath; - - if (cardinality === 'manyToOne') { - hasManyPath = buildCollectionName(descriptor.type); - } else { - const collectionName = buildCollectionName(modelClass.modelName); - const docId = resourceHash.id; - - hasManyPath = `${collectionName}/${docId}/${name}`; - } - - links[name] = hasManyPath; - } - }); - - newResourceHash.links = links; - - return super.extractRelationships(modelClass, newResourceHash); - } - - public serializeBelongsTo( - snapshot: DS.Snapshot, - json: { [key: string]: string | null | firebase.firestore.DocumentReference }, - relationship: RelationshipDefinition, - ): void { - super.serializeBelongsTo(snapshot, json, relationship); - - if (json[relationship.key]) { - const db = this.firebase.firestore(); - const docId = json[relationship.key] as string; - - if (relationship.options.buildReference) { - json[relationship.key] = relationship.options.buildReference(db).doc(docId); - } else { - const collectionName = buildCollectionName(relationship.type); - const path = `${collectionName}/${docId}`; - - json[relationship.key] = db.doc(path); - } - } - } - - public serialize(snapshot: DS.Snapshot, options: {}): {} { - const json: { [key: string]: unknown } = { ...super.serialize(snapshot, options) }; - - snapshot.eachRelationship((name: string, relationship) => { - if (relationship.kind === 'hasMany') { - delete json[name]; - } - }); - - return json; - } -} - -declare module 'ember-data/types/registries/serializer' { - export default interface SerializerRegistry { - 'cloud-firestore': CloudFirestoreSerializer; - } -} diff --git a/addon/services/-firebase.d.ts b/addon/services/-firebase.d.ts deleted file mode 100644 index eede6462..00000000 --- a/addon/services/-firebase.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint @typescript-eslint/no-empty-interface: 'off' */ - -import firebase from 'firebase/compat/app'; - -interface FirebaseInterface extends firebase.app.App {} - -declare module 'ember-cloud-firestore-adapter/services/-firebase' { - export default interface FirebaseService extends FirebaseInterface {} -} - -declare module '@ember/service' { - interface Registry { - '-firebase': FirebaseInterface; - } -} diff --git a/addon/services/-firebase.ts b/addon/services/-firebase.ts deleted file mode 100644 index d3d62ff4..00000000 --- a/addon/services/-firebase.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getOwner } from '@ember/application'; -import ApplicationInstance from '@ember/application/instance'; - -import firebase from 'firebase/compat/app'; -import 'firebase/compat/auth'; -import 'firebase/compat/firestore'; - -function setupEnvFirebase() { - if (typeof FastBoot === 'undefined') { - return firebase; - } - - const envFirebase = FastBoot.require('firebase/compat/app'); - - FastBoot.require('firebase/compat/auth'); - FastBoot.require('firebase/compat/firestore'); - - return envFirebase; -} - -export default { - isServiceFactory: true, - - create(context: ApplicationInstance): firebase.app.App { - const config = getOwner(context).resolveRegistration('config:environment'); - const envFirebase = setupEnvFirebase(); - let firebaseApp; - - try { - firebaseApp = envFirebase.app(); - } catch (e) { - firebaseApp = envFirebase.initializeApp(config['ember-cloud-firestore-adapter'].firebaseConfig); - } - - return firebaseApp; - }, -}; diff --git a/app/adapters/cloud-firestore.js b/app/adapters/cloud-firestore.js deleted file mode 100644 index 5c238a45..00000000 --- a/app/adapters/cloud-firestore.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-cloud-firestore-adapter/adapters/cloud-firestore'; diff --git a/app/serializers/cloud-firestore.js b/app/serializers/cloud-firestore.js deleted file mode 100644 index c1874740..00000000 --- a/app/serializers/cloud-firestore.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-cloud-firestore-adapter/serializers/cloud-firestore'; diff --git a/app/services/-firebase.js b/app/services/-firebase.js deleted file mode 100644 index ccad0b6b..00000000 --- a/app/services/-firebase.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-cloud-firestore-adapter/services/-firebase'; diff --git a/docs/authentication.md b/docs/authentication.md index bd7d065a..8b1b4893 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -6,7 +6,7 @@ The adapter supports Firebase authentication through the [ember-simple-auth](htt Using the `session` service provided by ember-simple-auth, a callback must be passed-in which will be responsible for authenticating the user. -The callback will have the [`firebase.auth.Auth`](https://firebase.google.com/docs/reference/js/firebase.auth.Auth) as a param. Use this to authenticate the user using any of the providers available. It **must** also return a promise that will resolve to an instance of [`UserCredential`](https://firebase.google.com/docs/reference/js/auth.usercredential). +The callback will have the [`Auth`](https://firebase.google.com/docs/reference/js/auth.auth) as a param. Use this to authenticate the user using any of the providers available. It **must** also return a promise that will resolve to an instance of [`UserCredential`](https://firebase.google.com/docs/reference/js/auth.usercredential). ```javascript import { signInWithEmailAndPassword } from 'ember-cloud-firestore-adapter/firebase/auth'; diff --git a/docs/create-update-delete-records.md b/docs/create-update-delete-records.md index 8ae44149..0098e150 100644 --- a/docs/create-update-delete-records.md +++ b/docs/create-update-delete-records.md @@ -40,7 +40,7 @@ Hook for providing a custom collection reference on where you want to save the d | Name | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------ | ----------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ### `include` @@ -53,7 +53,7 @@ Hook for providing additional documents to batch write | Name | Type | Description | | ----- | -------------------------------------------------------------------------------------------------------------- | ----------- | | batch | [`WriteBatch`](https://firebase.google.com/docs/reference/js/firestore_.writebatch) | | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ## `deleteRecord` @@ -84,7 +84,7 @@ Hook for providing a custom collection reference on where the document to be del | Name | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------ | ----------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ### `include` @@ -97,7 +97,7 @@ Hook for providing additional documents to batch write | Name | Type | Description | | ----- | -------------------------------------------------------------------------------------------------------------- | ----------- | | batch | [`WriteBatch`](https://firebase.google.com/docs/reference/js/firestore_.writebatch) | | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ## `destroyRecord` @@ -127,7 +127,7 @@ Hook for providing a custom collection reference on where the document to be del | Name | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------ | ----------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ### `include` @@ -140,7 +140,7 @@ Hook for providing additional documents to batch write | Name | Type | Description | | ----- | -------------------------------------------------------------------------------------------------------------- | ----------- | | batch | [`WriteBatch`](https://firebase.google.com/docs/reference/js/firestore_.writebatch) | | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ## Updating a record @@ -179,7 +179,7 @@ Hook for providing a custom collection reference on where the document to be upd | Name | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------ | ----------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ### `include` @@ -192,7 +192,7 @@ Hook for providing additional documents to batch write | Name | Type | Description | | ----- | -------------------------------------------------------------------------------------------------------------- | ----------- | | batch | [`WriteBatch`](https://firebase.google.com/docs/reference/js/firestore_.writebatch) | | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ## Saving relationships diff --git a/docs/finding-records.md b/docs/finding-records.md index f12fcb01..f1e6ecdc 100644 --- a/docs/finding-records.md +++ b/docs/finding-records.md @@ -38,7 +38,7 @@ Hook for providing a custom collection reference on where you want to fetch the | Name | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------ | ----------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ### Error Handling @@ -130,7 +130,7 @@ Hook for providing a custom collection reference on where you want to fetch the | Name | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------ | ----------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ### `filter` diff --git a/docs/getting-started.md b/docs/getting-started.md index 5035854e..7ef53464 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -47,14 +47,16 @@ The config object of your Firebase web app project. You can get this in the Proj This contains the settings related to Firestore. The available properties are: -- `settings` (optional) - An object representing [`firebase.firestore.Settings`](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.Settings). Any settings available there, you can set it here. -- `emulator` (optional) - Use this object property if you want to use [Firebase Emulator](https://firebase.google.com/docs/emulator-suite) for your local development. The available properties are `hostname` and `port`. +- `isCustomSetup` (optional) - A boolean to indicate whether you want to setup your Firestore instance on your own. +- `settings` (optional) - An object representing [`FirestoreSettings`](https://firebase.google.com/docs/reference/js/firestore_.firestoresettings.md#firestoresettings_interface). Any settings available there, you can set it here. +- `emulator` (optional) - Use this object property if you want to use [Firebase Emulator](https://firebase.google.com/docs/emulator-suite) for your local development. The available properties are `hostname`, `port`, and `options`. #### `auth` This contains the settings related to Auth. The available properties are: -- `emulator` (optional) - Use this object property if you want to use [Firebase Emulator](https://firebase.google.com/docs/emulator-suite) for your local development. The available properties are `hostname` and `port`. +- `isCustomSetup` (optional) - A boolean to indicate whether you want to setup your Auth instance on your own. +- `emulator` (optional) - Use this object property if you want to use [Firebase Emulator](https://firebase.google.com/docs/emulator-suite) for your local development. The available properties are `hostname`, `port`, and `options`. ## 2. Create Your Application Adapter @@ -80,10 +82,6 @@ These are the settings currently available: - `referenceKeyName` - Name of the field that will indicate whether a document is a reference to another one. (Defaults to `'referenceTo'`) -> **NOTE:** This addon is in a transition phase towards Firebase Modular SDK. In order to support it alongside the Compat SDK, we've created a new `cloud-firestore-modular` adapter. Compat SDK adapter is now in maintenance mode until Firebase v10 so we recommend everyone to use the Modular SDK adapter instead moving forward. -> -> To view the Compat SDK docs, click [here](https://github.com/mikkopaderes/ember-cloud-firestore-adapter/blob/v2.0.2/docs/getting-started.md). - ## 3. Create Your Application Serializer Create an application serializer by running: @@ -100,10 +98,6 @@ import CloudFirestoreSerializer from 'ember-cloud-firestore-adapter/serializers/ export default class ApplicationSerializer extends CloudFirestoreSerializer { } ``` -> **NOTE:** This addon is in a transition phase towards Firebase Modular SDK. In order to support it alongside the Compat SDK, we've created a new `cloud-firestore-modular` serializer. Compat SDK serializer is now in maintenance mode until Firebase v10 so we recommend everyone to use the Modular SDK serializer instead moving forward. -> -> To view the Compat SDK docs, click [here](https://github.com/mikkopaderes/ember-cloud-firestore-adapter/blob/v2.0.2/docs/getting-started.md). - ## 4. Firebase and Auth Modular API Imports In order to support FastBoot, we've created wrapper imports for the Modular API functions which you can source out from `ember-cloud-firestore-adapter/firebase/` respectively. diff --git a/docs/relationships.md b/docs/relationships.md index 8b58fa3f..da8e6ff8 100644 --- a/docs/relationships.md +++ b/docs/relationships.md @@ -31,7 +31,7 @@ Hook for providing a custom collection reference. | Name | Type | Description | | -------| ------------------------------------------------------------------------------------------------------------ | ----------------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ## `hasMany` @@ -74,7 +74,7 @@ Hook for providing a custom collection reference. | Name | Type | Description | | -------| ------------------------------------------------------------------------------------------------------------ | ----------------- | -| db | [`firebase.firestore.Firestore`](https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore) | | +| db | [`Firestore`](https://firebase.google.com/docs/reference/js/firestore_.firestore) | | ### `filter` diff --git a/docs/testing.md b/docs/testing.md index d9773322..c9cf20a2 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,7 +4,7 @@ We use [Firebase Local Emulator Suite](https://firebase.google.com/docs/emulator ## Setup Addon to Use Emulator -Add an `ember-cloud-firestore-adapter.firestore.emulator` property in your `config/environment.js` and make sure to disable it in production environment. +Add an `ember-cloud-firestore-adapter.firestore.emulator` property in your `config/environment.js` and **make sure to disable it in production environment**. ```javascript let ENV = { diff --git a/tests/acceptance/features-test.ts b/tests/acceptance/features-test.ts index 97c5657c..c66a1116 100644 --- a/tests/acceptance/features-test.ts +++ b/tests/acceptance/features-test.ts @@ -2,17 +2,18 @@ import { click, visit, waitFor } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import firebase from 'firebase/compat/app'; +import { Firestore } from 'firebase/firestore'; +import { doc, getDoc, getFirestore } from 'ember-cloud-firestore-adapter/firebase/firestore'; import resetFixtureData from '../helpers/reset-fixture-data'; module('Acceptance | features', function (hooks) { - let db: firebase.firestore.Firestore; + let db: Firestore; setupApplicationTest(hooks); hooks.beforeEach(async function () { - db = this.owner.lookup('service:-firebase').firestore(); + db = getFirestore(); await resetFixtureData(db); }); @@ -80,7 +81,7 @@ module('Acceptance | features', function (hooks) { assert.dom('[data-test-name="user_a"]').hasText('user_a'); assert.dom('[data-test-age="user_a"]').hasNoText(); - const createdRecord = await db.doc('posts/new_post').get(); + const createdRecord = await getDoc(doc(db, 'posts/new_post')); assert.strictEqual(createdRecord.get('publisher').path, 'publishers/user_a'); }); diff --git a/tests/dummy/app/controllers/application.ts b/tests/dummy/app/controllers/application.ts index 4dab550d..fd6a30c2 100644 --- a/tests/dummy/app/controllers/application.ts +++ b/tests/dummy/app/controllers/application.ts @@ -2,8 +2,8 @@ import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; +import { Auth } from 'firebase/auth'; import SessionService from 'ember-simple-auth/services/session'; -import firebase from 'firebase/compat/app'; import { createUserWithEmailAndPassword, @@ -18,7 +18,7 @@ export default class ApplicationController extends Controller { @action login(): void { - this.session.authenticate('authenticator:firebase', (auth: firebase.auth.Auth) => ( + this.session.authenticate('authenticator:firebase', (auth: Auth) => ( createUserWithEmailAndPassword(auth, 'foo@gmail.com', 'foobar') ).then((credential) => credential.user).catch(() => ( signInWithEmailAndPassword(auth, 'foo@gmail.com', 'foobar') diff --git a/tests/dummy/app/controllers/features.ts b/tests/dummy/app/controllers/features.ts index 6c114ca6..2ceddc08 100644 --- a/tests/dummy/app/controllers/features.ts +++ b/tests/dummy/app/controllers/features.ts @@ -5,8 +5,7 @@ import Controller from '@ember/controller'; import EmberArray from '@ember/array'; import Store from '@ember-data/store'; -import { CollectionReference } from 'firebase/firestore'; -import firebase from 'firebase/compat/app'; +import { CollectionReference, Firestore } from 'firebase/firestore'; import { collection, query, where } from 'ember-cloud-firestore-adapter/firebase/firestore'; import UserModel from '../models/user'; @@ -103,7 +102,7 @@ export default class FeaturesController extends Controller { @action public async handleQuery1Click(): Promise { const users = await this.store.query('user', { - buildReference(db: firebase.firestore.Firestore) { + buildReference(db: Firestore) { return collection(db, 'users'); }, @@ -118,7 +117,7 @@ export default class FeaturesController extends Controller { @action public async handleQuery2Click(): Promise { const users = await this.store.query('user', { - buildReference(db: firebase.firestore.Firestore) { + buildReference(db: Firestore) { return collection(db, 'users/user_a/foobar'); }, }); diff --git a/tests/dummy/app/models/post.ts b/tests/dummy/app/models/post.ts index 74f1c1bf..52a7432e 100644 --- a/tests/dummy/app/models/post.ts +++ b/tests/dummy/app/models/post.ts @@ -8,7 +8,7 @@ import DS from 'ember-data'; import Model, { attr, belongsTo } from '@ember-data/model'; -import firebase from 'firebase/compat/app'; +import { Firestore } from 'firebase/firestore'; import { collection } from 'ember-cloud-firestore-adapter/firebase/firestore'; import TimestampTransform from 'ember-cloud-firestore-adapter/transforms/timestamp'; @@ -32,7 +32,7 @@ export default class PostModel extends Model { inverse: null, // @ts-ignore: TODO - find a way to set custom property in RelationshipOptions interface - buildReference(db: firebase.firestore.Firestore) { + buildReference(db: Firestore) { return collection(db, 'publishers'); }, }) diff --git a/tests/helpers/reset-fixture-data.ts b/tests/helpers/reset-fixture-data.ts index 02bcdda6..31b39d60 100644 --- a/tests/helpers/reset-fixture-data.ts +++ b/tests/helpers/reset-fixture-data.ts @@ -1,60 +1,62 @@ -import firebase from 'firebase/compat/app'; +import { Firestore } from 'firebase/firestore'; -export default async function resetFixtureData(db: firebase.firestore.Firestore): Promise { +import { doc, writeBatch } from 'ember-cloud-firestore-adapter/firebase/firestore'; + +export default async function resetFixtureData(db: Firestore): Promise { await fetch('http://localhost:8080/emulator/v1/projects/ember-cloud-firestore-adapter-test-project/databases/(default)/documents', { method: 'DELETE', }); - const batch = db.batch(); + const batch = writeBatch(db); const testData = { 'admins/user_a': { since: 2010 }, 'admins/user_b': { since: 2015 }, 'users/user_a': { name: 'user_a', age: 15, username: 'user_a' }, - 'users/user_a/groups/group_a': { referenceTo: db.doc('groups/group_a') }, + 'users/user_a/groups/group_a': { referenceTo: doc(db, 'groups/group_a') }, 'users/user_a/feeds/post_b': { title: 'post_b', createdOn: new Date(), approvedBy: 'user_a', - author: db.doc('users/user_b'), - group: db.doc('groups/group_a'), + author: doc(db, 'users/user_b'), + group: doc(db, 'groups/group_a'), }, - 'users/user_a/friends/user_b': { referenceTo: db.doc('users/user_b') }, - 'users/user_a/friends/user_c': { referenceTo: db.doc('users/user_c') }, + 'users/user_a/friends/user_b': { referenceTo: doc(db, 'users/user_b') }, + 'users/user_a/friends/user_c': { referenceTo: doc(db, 'users/user_c') }, 'users/user_b': { name: 'user_b', age: 10, username: 'user_b' }, - 'users/user_b/friends/user_a': { referenceTo: db.doc('users/user_a') }, - 'users/user_b/groups/group_a': { referenceTo: db.doc('groups/group_a') }, + 'users/user_b/friends/user_a': { referenceTo: doc(db, 'users/user_a') }, + 'users/user_b/groups/group_a': { referenceTo: doc(db, 'groups/group_a') }, 'users/user_c': { name: 'user_c', age: 20, username: 'user_c' }, - 'users/user_c/friends/user_a': { referenceTo: db.doc('users/user_a') }, + 'users/user_c/friends/user_a': { referenceTo: doc(db, 'users/user_a') }, 'groups/group_a': { name: 'group_a' }, - 'groups/group_a/members/user_a': { referenceTo: db.doc('users/user_a') }, - 'groups/group_a/members/user_b': { referenceTo: db.doc('users/user_b') }, + 'groups/group_a/members/user_a': { referenceTo: doc(db, 'users/user_a') }, + 'groups/group_a/members/user_b': { referenceTo: doc(db, 'users/user_b') }, 'groups/group_b': { name: 'group_b' }, 'posts/post_a': { title: 'post_a', createdOn: new Date(), approvedBy: 'user_a', - author: db.doc('users/user_a'), - group: db.doc('groups/group_a'), + author: doc(db, 'users/user_a'), + group: doc(db, 'groups/group_a'), }, 'posts/post_b': { title: 'post_b', createdOn: new Date(), approvedBy: 'user_a', - author: db.doc('users/user_b'), - group: db.doc('groups/group_a'), + author: doc(db, 'users/user_b'), + group: doc(db, 'groups/group_a'), }, 'posts/post_c': { title: 'post_c', createdOn: new Date(), approvedBy: 'user_b', - author: db.doc('users/user_a'), - group: db.doc('groups/group_a'), + author: doc(db, 'users/user_a'), + group: doc(db, 'groups/group_a'), }, }; const keys = Object.keys(testData) as Array; - keys.forEach((key) => batch.set(db.doc(key), testData[key])); + keys.forEach((key) => batch.set(doc(db, key), testData[key])); await batch.commit(); } diff --git a/tests/unit/-private/flatten-doc-snapshot-test.ts b/tests/unit/-private/flatten-doc-snapshot-test.ts index bcec4f1f..296de747 100644 --- a/tests/unit/-private/flatten-doc-snapshot-test.ts +++ b/tests/unit/-private/flatten-doc-snapshot-test.ts @@ -1,6 +1,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import { doc, getDoc, getFirestore } from 'ember-cloud-firestore-adapter/firebase/firestore'; import flattenDocSnapshot from 'ember-cloud-firestore-adapter/-private/flatten-doc-snapshot'; module('Unit | -Private | flatten-doc-snapshot-data', function (hooks) { @@ -8,10 +9,9 @@ module('Unit | -Private | flatten-doc-snapshot-data', function (hooks) { test('should return a merged doc snapshot ID and data in a single object', async function (assert) { // Arrange - const firebase = this.owner.lookup('service:-firebase'); - const db = firebase.firestore(); + const db = getFirestore(); - const snapshot = await db.doc('users/user_a').get(); + const snapshot = await getDoc(doc(db, 'users/user_a')); // Act const result = flattenDocSnapshot(snapshot); diff --git a/tests/unit/-private/realtime-tracker-test.ts b/tests/unit/-private/realtime-tracker-test.ts deleted file mode 100644 index 10d556da..00000000 --- a/tests/unit/-private/realtime-tracker-test.ts +++ /dev/null @@ -1,317 +0,0 @@ -/* eslint ember/use-ember-data-rfc-395-imports: off */ - -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; -import ArrayProxy from '@ember/array/proxy'; -import DS from 'ember-data'; -import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; -import RSVP from 'rsvp'; - -import firebase from 'firebase/compat/app'; -import sinon from 'sinon'; - -import RealtimeTracker from 'ember-cloud-firestore-adapter/-private/realtime-tracker'; -import resetFixtureData from '../../helpers/reset-fixture-data'; - -module('Unit | -Private | realtime-tracker', function (hooks) { - let db: firebase.firestore.Firestore; - - setupTest(hooks); - - hooks.beforeEach(async function () { - db = this.owner.lookup('service:-firebase').firestore(); - - await resetFixtureData(db); - }); - - hooks.afterEach(async function () { - await resetFixtureData(db); - }); - - module('trackFindRecordChanges()', function () { - test('should not push record to store when called for the first time', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - - const realtimeTracker = new RealtimeTracker(store); - const docRef = db.doc('users/user_a'); - - // Act - realtimeTracker.trackFindRecordChanges('user', docRef); - - // Assert - setTimeout(() => { - assert.strictEqual(store.peekRecord('user', 'user_a'), null); - done(); - }, 500); - }); - - test('should push record to store when an update has been detected', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const realtimeTracker = new RealtimeTracker(store); - const docRef = db.doc('users/user_a'); - const newName = Math.random().toString(); - const storeFixture = { - data: { - id: 'user_a', - type: 'user', - attributes: { - name: 'user_a', - }, - relationships: { - groups: { - links: { - related: 'users/user_a/groups', - }, - }, - posts: { - links: { - related: 'posts', - }, - }, - friends: { - links: { - related: 'users/user_a/friends', - }, - }, - }, - }, - }; - - store.push(storeFixture); - - // Act - realtimeTracker.trackFindRecordChanges('user', docRef); - - setTimeout(async () => { - await docRef.update({ name: newName }); - - // Assert - setTimeout(() => { - assert.strictEqual(store.peekRecord('user', 'user_a').name, newName); - done(); - }, 500); - }, 500); - }); - - test('should unload record from store when a delete has been detected', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const realtimeTracker = new RealtimeTracker(store); - const docRef = db.doc('users/user_a'); - const storeFixture = { - data: { - id: 'user_a', - type: 'user', - attributes: { - name: 'user_a', - }, - relationships: { - groups: { - links: { - related: 'users/user_a/groups', - }, - }, - posts: { - links: { - related: 'posts', - }, - }, - friends: { - links: { - related: 'users/user_a/friends', - }, - }, - }, - }, - }; - - store.push(storeFixture); - - // Act - realtimeTracker.trackFindRecordChanges('user', docRef); - - setTimeout(async () => { - await docRef.delete(); - - // Assert - setTimeout(() => { - assert.strictEqual(store.peekRecord('user', 'user_a'), null); - done(); - }, 500); - }, 500); - }); - }); - - module('trackFindAllChanges()', function () { - test('should not find individual records via store when called for the first time', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const findRecordStub = sinon.stub(store, 'findRecord'); - const realtimeTracker = new RealtimeTracker(store); - const collectionRef = db.collection('users'); - - // Act - realtimeTracker.trackFindAllChanges('user', collectionRef); - - // Assert - setTimeout(() => { - assert.ok(findRecordStub.notCalled); - done(); - }, 500); - }); - - test('should find individual records via store when an update has been detected', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const findRecordStub = sinon.stub(store, 'findRecord'); - const realtimeTracker = new RealtimeTracker(store); - const collectionRef = db.collection('users'); - - // Act - realtimeTracker.trackFindAllChanges('user', collectionRef); - - setTimeout(async () => { - await db.doc('users/new_user').set({ name: 'new_user' }); - - // Assert - setTimeout(() => { - assert.ok(findRecordStub.called); - done(); - }, 500); - }, 500); - }); - }); - - module('trackFindHasManyChanges()', function () { - test('should not reload has many reference when called for the first time', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const reloadStub = sinon.stub().returns(Promise.resolve()); - - sinon.stub(store, 'peekRecord').withArgs('user', 'user_a').returns({ - hasMany: sinon.stub().withArgs('groups').returns({ reload: reloadStub }), - }); - - const realtimeTracker = new RealtimeTracker(store); - const collectionRef = db.collection('groups'); - - // Act - realtimeTracker.trackFindHasManyChanges('user', 'user_a', 'groups', collectionRef); - - // Assert - setTimeout(() => { - assert.ok(reloadStub.notCalled); - done(); - }, 500); - }); - - test('should reload has many reference when an update was detected', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const reloadStub = sinon.stub().returns(Promise.resolve()); - - sinon.stub(store, 'peekRecord').withArgs('user', 'user_a').returns({ - hasMany: sinon.stub().withArgs('groups').returns({ reload: reloadStub }), - }); - - const realtimeTracker = new RealtimeTracker(store); - const collectionRef = db.collection('groups'); - - // Act - realtimeTracker.trackFindHasManyChanges('user', 'user_a', 'groups', collectionRef); - - setTimeout(async () => { - await db.doc('groups/new_group').set({ name: 'new_group' }); - - // Assert - setTimeout(() => { - assert.ok(reloadStub.called); - done(); - }, 500); - }, 500); - }); - }); - - module('trackQueryChanges()', function () { - test('should not update record array when called for the first time', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const recordArray = {} as DS.AdapterPopulatedRecordArray; - const promiseArray = ArrayProxy.extend(PromiseProxyMixin); - const updateStub = sinon.stub().returns( - promiseArray.create({ promise: RSVP.Promise.resolve() }), - ); - - recordArray.update = updateStub; - - const realtimeTracker = new RealtimeTracker(store); - const query = db.collection('groups').where('name', '==', 'new_group'); - - // Act - realtimeTracker.trackQueryChanges(query, recordArray); - - // Assert - setTimeout(() => { - assert.ok(updateStub.notCalled); - done(); - }, 500); - }); - - test('should update record array when an update was detected', async function (assert) { - // Arrange - assert.expect(1); - - const done = assert.async(); - const store = this.owner.lookup('service:store'); - const recordArray = {} as DS.AdapterPopulatedRecordArray; - const promiseArray = ArrayProxy.extend(PromiseProxyMixin); - const updateStub = sinon.stub().returns( - promiseArray.create({ promise: RSVP.Promise.resolve() }), - ); - - recordArray.update = updateStub; - - const realtimeTracker = new RealtimeTracker(store); - const query = db.collection('groups').where('name', '==', 'new_group'); - - // Act - realtimeTracker.trackQueryChanges(query, recordArray); - - setTimeout(async () => { - await db.doc('groups/new_group').set({ name: 'new_group' }); - - // Assert - setTimeout(() => { - assert.ok(updateStub.called); - done(); - }, 500); - }, 500); - }); - }); -}); diff --git a/tests/unit/adapters/cloud-firestore-modular-test.ts b/tests/unit/adapters/cloud-firestore-modular-test.ts index 6921d86b..769a2253 100644 --- a/tests/unit/adapters/cloud-firestore-modular-test.ts +++ b/tests/unit/adapters/cloud-firestore-modular-test.ts @@ -3,13 +3,13 @@ import { setupTest } from 'ember-qunit'; import EmberObject from '@ember/object'; import { CollectionReference, Firestore, WriteBatch } from 'firebase/firestore'; -import firebase from 'firebase/compat/app'; import sinon from 'sinon'; import { collection, doc, getDoc, + getFirestore, limit, query, where, @@ -18,12 +18,12 @@ import { AdapterRecordNotFoundError } from 'ember-cloud-firestore-adapter/utils/ import resetFixtureData from '../../helpers/reset-fixture-data'; module('Unit | Adapter | cloud firestore modular', function (hooks) { - let db: firebase.firestore.Firestore; + let db: Firestore; setupTest(hooks); hooks.beforeEach(async function () { - db = this.owner.lookup('service:-firebase').firestore(); + db = getFirestore(); await resetFixtureData(db); }); diff --git a/tests/unit/adapters/cloud-firestore-test.ts b/tests/unit/adapters/cloud-firestore-test.ts deleted file mode 100644 index 7d054dcd..00000000 --- a/tests/unit/adapters/cloud-firestore-test.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; -import EmberObject from '@ember/object'; - -import firebase from 'firebase/compat/app'; -import sinon from 'sinon'; - -import resetFixtureData from '../../helpers/reset-fixture-data'; - -module('Unit | Adapter | cloud firestore', function (hooks) { - let db: firebase.firestore.Firestore; - - setupTest(hooks); - - hooks.beforeEach(async function () { - db = this.owner.lookup('service:-firebase').firestore(); - - await resetFixtureData(db); - }); - - hooks.afterEach(async function () { - await resetFixtureData(db); - }); - - module('function: generateIdForRecord', function () { - test('should generate ID for record', function (assert) { - // Arrange - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = adapter.generateIdForRecord({}, 'foo'); - - // Assert - assert.strictEqual(typeof result, 'string'); - }); - }); - - module('function: createRecord', function () { - test('should proxy a call to updateRecord and return with the created doc', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const snapshot = { id: 'user_100', age: 30, username: 'user_100' }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - const updateRecordStub = sinon.stub(adapter, 'updateRecord').returns('foo'); - - // Act - const result = await adapter.createRecord(store, modelClass, snapshot); - - // Assert - assert.strictEqual(result, 'foo'); - assert.ok(updateRecordStub.calledWithExactly(store, modelClass, snapshot)); - }); - }); - - module('function: updateRecord', function () { - test('should update record and resolve with the updated doc', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const snapshot = { - id: 'user_a', - age: 50, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - adapter.serialize = sinon.stub().returns({ - age: 50, - username: 'user_a', - }); - - // Act - const result = await adapter.updateRecord(store, modelClass, snapshot); - - // Assert - assert.deepEqual(result, { age: 50, username: 'user_a' }); - - const userA = await db.collection('users').doc('user_a').get(); - - assert.strictEqual(userA.get('age'), 50); - assert.strictEqual(userA.get('username'), 'user_a'); - }); - - test('should update record in a custom collection and resolve with the updated resource', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const snapshot = { - id: 'user_a', - age: 50, - adapterOptions: { - buildReference(firestore: firebase.firestore.Firestore) { - return firestore.collection('foobar'); - }, - }, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - adapter.serialize = sinon.stub().returns({ age: 50 }); - - // Act - const result = await adapter.createRecord(store, modelClass, snapshot); - - // Assert - assert.deepEqual(result, { age: 50 }); - - const user100 = await db.collection('foobar').doc('user_a').get(); - - assert.deepEqual(user100.data(), { age: 50 }); - }); - - test('should update record and process additional batched writes', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const snapshot = { - id: 'user_a', - age: 50, - adapterOptions: { - include(batch: firebase.firestore.WriteBatch, firestore: firebase.firestore.Firestore) { - batch.set(firestore.collection('users').doc('user_100'), { age: 60 }); - }, - }, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - adapter.generateIdForRecord = sinon.stub().returns('12345'); - adapter.serialize = sinon.stub().returns({ age: 50, username: 'user_a' }); - - // Act - const result = await adapter.updateRecord(store, modelClass, snapshot); - - // Assert - assert.deepEqual(result, { age: 50, username: 'user_a' }); - - const userA = await db.collection('users').doc('user_a').get(); - - assert.deepEqual(userA.data(), { age: 50, name: 'user_a', username: 'user_a' }); - - const user100 = await db.collection('users').doc('user_100').get(); - - assert.deepEqual(user100.data(), { age: 60 }); - }); - }); - - module('function: deleteRecord', function () { - test('should delete record', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const snapshot = { id: 'user_a' }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - await adapter.deleteRecord(store, modelClass, snapshot); - - // Assert - const userA = await db.collection('users').doc('user_a').get(); - - assert.notOk(userA.exists); - }); - - test('should delete record in a custom collection', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'post' }; - const snapshot = { - id: 'post_b', - - adapterOptions: { - buildReference(firestore: firebase.firestore.Firestore) { - return firestore.collection('users').doc('user_a').collection('feeds'); - }, - }, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - await adapter.deleteRecord(store, modelClass, snapshot); - - // Assert - const postB = await db.doc('users/user_a/feeds/post_b').get(); - - assert.notOk(postB.exists); - }); - - test('should delete record and process additional batched writes', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const snapshot = { - id: 'user_a', - adapterOptions: { - include(batch: firebase.firestore.WriteBatch, firestore: firebase.firestore.Firestore) { - batch.delete(firestore.collection('users').doc('user_b')); - }, - }, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - adapter.serialize = sinon.stub().returns({ age: 50, username: 'user_a' }); - - // Act - await adapter.deleteRecord(store, modelClass, snapshot); - - // Assert - const userA = await db.collection('users').doc('user_a').get(); - - assert.notOk(userA.exists); - - const userB = await db.collection('users').doc('user_b').get(); - - assert.notOk(userB.exists); - }); - }); - - module('function: findAll', function () { - test('should fetch all records for a model', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const modelClass = { modelName: 'user' }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findAll(store, modelClass); - - // Assert - assert.deepEqual(result, [ - { - id: 'user_a', - age: 15, - name: 'user_a', - username: 'user_a', - }, - { - id: 'user_b', - age: 10, - name: 'user_b', - username: 'user_b', - }, - { - id: 'user_c', - age: 20, - name: 'user_c', - username: 'user_c', - }, - ]); - }); - }); - - module('function: findRecord', function () { - test('should fetch a record', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const modelClass = { modelName: 'user' }; - const modelId = 'user_a'; - const snapshot = {}; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findRecord(store, modelClass, modelId, snapshot); - - // Assert - assert.deepEqual(result, { - id: 'user_a', - age: 15, - name: 'user_a', - username: 'user_a', - }); - }); - - test('should fetch a record in a custom collection', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const modelClass = { modelName: 'user' }; - const modelId = 'user_a'; - const snapshot = { - adapterOptions: { - buildReference(firestore: firebase.firestore.Firestore) { - return firestore.collection('admins'); - }, - }, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findRecord(store, modelClass, modelId, snapshot); - - // Assert - assert.deepEqual(result, { id: 'user_a', since: 2010 }); - }); - - test('should throw an error when record does not exists', async function (assert) { - // Arrange - assert.expect(1); - - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const modelClass = { modelName: 'user' }; - const modelId = 'user_100'; - const snapshot = {}; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - try { - // Act - await adapter.findRecord(store, modelClass, modelId, snapshot); - } catch (error) { - // Assert - assert.strictEqual(error.message, 'Record user_100 for model type user doesn\'t exist'); - } - }); - }); - - module('function: findBelongsTo', function () { - test('should fetch a belongs to record', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const snapshot = {}; - const url = 'users/user_a'; - const relationship = { type: 'user', options: {} }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findBelongsTo(store, snapshot, url, relationship); - - // Assert - assert.deepEqual(result, { - id: 'user_a', - age: 15, - name: 'user_a', - username: 'user_a', - }); - }); - }); - - module('function: findHasMany', function () { - test('should fetch many-to-one cardinality', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const determineRelationshipTypeStub = sinon.stub().returns('manyToOne'); - const inverseForStub = sinon.stub().returns({ name: 'author' }); - const snapshot = { - id: 'user_a', - modelName: 'user', - record: EmberObject.create(), - type: { - determineRelationshipType: determineRelationshipTypeStub, - inverseFor: inverseForStub, - }, - }; - const url = 'posts'; - const relationship = { - key: 'posts', - options: { - filter(reference: firebase.firestore.CollectionReference) { - return reference.limit(1); - }, - }, - type: 'post', - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findHasMany(store, snapshot, url, relationship); - - // Assert - assert.strictEqual(result[0].id, 'post_a'); - assert.strictEqual(result[0].title, 'post_a'); - assert.ok(determineRelationshipTypeStub.calledWithExactly(relationship, store)); - assert.ok(inverseForStub.calledWithExactly(relationship.key, store)); - }); - - test('should fetch many-to-whatever cardinality', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const determineRelationshipTypeStub = sinon.stub().returns('manyToNone'); - const snapshot = { - record: EmberObject.create({ - referenceTo: db.collection('users').doc('user_a'), - }), - type: { - determineRelationshipType: determineRelationshipTypeStub, - }, - }; - const url = 'users/user_a/friends'; - const relationship = { - options: { - filter(reference: firebase.firestore.CollectionReference) { - return reference.limit(1); - }, - }, - type: 'user', - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findHasMany(store, snapshot, url, relationship); - - // Assert - assert.deepEqual(result, [ - { - id: 'user_b', - age: 10, - name: 'user_b', - username: 'user_b', - }, - ]); - assert.ok(determineRelationshipTypeStub.calledWithExactly(relationship, store)); - }); - - test('should be able to fetch with filter using a record property', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const determineRelationshipTypeStub = sinon.stub().returns('manyToOne'); - const inverseForStub = sinon.stub().returns({ name: 'author' }); - const snapshot = { - id: 'user_a', - modelName: 'user', - record: EmberObject.create({ - id: 'user_a', - }), - type: { - determineRelationshipType: determineRelationshipTypeStub, - inverseFor: inverseForStub, - }, - }; - const url = 'posts'; - const relationship = { - key: 'posts', - options: { - filter(reference: firebase.firestore.CollectionReference, record: { id: string }) { - return reference.where('approvedBy', '==', record.id); - }, - }, - type: 'post', - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findHasMany(store, snapshot, url, relationship); - - // Assert - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].id, 'post_a'); - assert.ok(determineRelationshipTypeStub.calledWithExactly(relationship, store)); - assert.ok(inverseForStub.calledWithExactly(relationship.key, store)); - }); - - test('should be able to fetch with a custom reference', async function (assert) { - // Arrange - const store = { normalize: sinon.stub(), push: sinon.stub() }; - const snapshot = { - record: EmberObject.create({ id: 'user_a' }), - }; - const url = null; - const relationship = { - key: 'userBFeeds', - options: { - buildReference(firestore: firebase.firestore.Firestore, record: { id: string }) { - return firestore.collection(`users/${record.id}/feeds`); - }, - - filter(reference: firebase.firestore.CollectionReference) { - return reference.limit(1); - }, - }, - type: 'post', - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.findHasMany(store, snapshot, url, relationship); - - // Assert - assert.strictEqual(result[0].id, 'post_b'); - assert.strictEqual(result[0].title, 'post_b'); - }); - }); - - module('function: query', function () { - test('should query for records', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const query = { - filter(reference: firebase.firestore.CollectionReference) { - return reference.where('age', '>=', 15).limit(1); - }, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.query(store, modelClass, query); - - // Assert - assert.deepEqual(result, [ - { - id: 'user_a', - age: 15, - name: 'user_a', - username: 'user_a', - }, - ]); - }); - - test('should query for records in a custom collection', async function (assert) { - // Arrange - const store = {}; - const modelClass = { modelName: 'user' }; - const query = { - buildReference(firestore: firebase.firestore.Firestore) { - return firestore.collection('admins'); - }, - - filter(reference: firebase.firestore.CollectionReference) { - return reference.where('since', '==', 2015); - }, - }; - const adapter = this.owner.lookup('adapter:cloud-firestore'); - - // Act - const result = await adapter.query(store, modelClass, query); - - // Assert - assert.deepEqual(result, [{ id: 'user_b', since: 2015 }]); - }); - }); -}); diff --git a/tests/unit/authenticators/firebase-test.ts b/tests/unit/authenticators/firebase-test.ts index 1f6ef1af..2110d83f 100644 --- a/tests/unit/authenticators/firebase-test.ts +++ b/tests/unit/authenticators/firebase-test.ts @@ -1,22 +1,22 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import firebase from 'firebase/compat/app'; +import { Auth } from 'firebase/auth'; +import { Firestore } from 'firebase/firestore'; -import { signInAnonymously, signOut } from 'ember-cloud-firestore-adapter/firebase/auth'; +import { getAuth, signInAnonymously, signOut } from 'ember-cloud-firestore-adapter/firebase/auth'; +import { getFirestore } from 'ember-cloud-firestore-adapter/firebase/firestore'; import resetFixtureData from '../../helpers/reset-fixture-data'; module('Unit | Authenticator | firebase', function (hooks) { - let auth: firebase.auth.Auth; - let db: firebase.firestore.Firestore; + let auth: Auth; + let db: Firestore; setupTest(hooks); hooks.beforeEach(async function () { - const app = this.owner.lookup('service:-firebase'); - - auth = app.auth(); - db = app.firestore(); + auth = getAuth(); + db = getFirestore(); await signInAnonymously(auth); await resetFixtureData(db); diff --git a/tests/unit/serializers/cloud-firestore-test.ts b/tests/unit/serializers/cloud-firestore-test.ts deleted file mode 100644 index 57a89683..00000000 --- a/tests/unit/serializers/cloud-firestore-test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Serializer | cloud-firestore', function (hooks) { - setupTest(hooks); - - module('extractRelationship()', function () { - test('should return object containing the type and id of a relationship', function (assert) { - // Arrange - const serializer = this.owner.lookup('serializer:cloud-firestore'); - - // Act - const result = serializer.extractRelationship('user', { - path: 'users/user_a', - firestore: {}, - }); - - // Assert - assert.deepEqual(result, { id: 'user_a', type: 'user' }); - }); - - test('should return null when without any relationship hash', function (assert) { - // Arrange - const serializer = this.owner.lookup('serializer:cloud-firestore'); - - // Act - const result = serializer.extractRelationship('user', null); - - // Assert - assert.deepEqual(result, null); - }); - }); - - // NOTE: Other public methods are hard to test because they rely on private APIs from ember-data -}); diff --git a/tests/unit/services/-firebase-test.ts b/tests/unit/services/-firebase-test.ts deleted file mode 100644 index c6779734..00000000 --- a/tests/unit/services/-firebase-test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Service | _firebase', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - const service = this.owner.lookup('service:-firebase'); - assert.ok(service); - }); -}); diff --git a/tests/unit/services/-firestore-data-manager-test.ts b/tests/unit/services/-firestore-data-manager-test.ts index 7a50e6eb..6a56cf10 100644 --- a/tests/unit/services/-firestore-data-manager-test.ts +++ b/tests/unit/services/-firestore-data-manager-test.ts @@ -6,13 +6,14 @@ import DS from 'ember-data'; import RSVP from 'rsvp'; import Store from '@ember-data/store'; -import firebase from 'firebase/compat/app'; +import { Firestore } from 'firebase/firestore'; import sinon from 'sinon'; import { collection, deleteDoc, doc, + getFirestore, query, updateDoc, } from 'ember-cloud-firestore-adapter/firebase/firestore'; @@ -20,12 +21,12 @@ import FirestoreDataManager from 'ember-cloud-firestore-adapter/services/-firest import resetFixtureData from '../../helpers/reset-fixture-data'; module('Unit | Service | -firestore-data-manager', function (hooks) { - let db: firebase.firestore.Firestore; + let db: Firestore; setupTest(hooks); hooks.beforeEach(async function () { - db = this.owner.lookup('service:-firebase').firestore(); + db = getFirestore(); await resetFixtureData(db); }); diff --git a/tests/unit/transforms/timestamp-test.ts b/tests/unit/transforms/timestamp-test.ts index 377a72ad..45c8b908 100644 --- a/tests/unit/transforms/timestamp-test.ts +++ b/tests/unit/transforms/timestamp-test.ts @@ -1,18 +1,20 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import firebase from 'firebase/compat/app'; +import { Firestore } from 'firebase/firestore'; -import { serverTimestamp } from 'ember-cloud-firestore-adapter/firebase/firestore'; +import { + doc, getDoc, getFirestore, serverTimestamp, +} from 'ember-cloud-firestore-adapter/firebase/firestore'; import resetFixtureData from 'dummy/tests/helpers/reset-fixture-data'; module('Unit | Transform | timestamp', function (hooks) { - let db: firebase.firestore.Firestore; + let db: Firestore; setupTest(hooks); hooks.beforeEach(async function () { - db = this.owner.lookup('service:-firebase').firestore(); + db = getFirestore(); await resetFixtureData(db); }); @@ -20,7 +22,7 @@ module('Unit | Transform | timestamp', function (hooks) { module('deserialize()', function () { test('should return result of value.toDate when value is an instance of firebase.firestore.Timestamp', async function (assert) { // Arrange - const post = await db.doc('posts/post_a').get(); + const post = await getDoc(doc(db, 'posts/post_a')); const transform = this.owner.lookup('transform:timestamp'); // Act