2

I am using Flamelink as a headless CMS integrated with Firebase. All of my string fields are working just fine; I am just having a little trouble getting a URL of the media that was uploaded to Firebase Storage.

The collection that I'm getting my string fields from is fl_content The fields are:

string1
string2
imageUpload

Within Firebase, I can see the data that gets saved from Flamelink:

string1: "Titanium Tee"
string2: "Lower your handicap by 50 with premium Titanium golf tees!"

imageUpload returns an array with a reference to fl_files (a different collection in Firebase)

imageUpload:
    0 fl_files/ZqVeXI3vX0rFDuJVDzuR

Under fl_files > ZqVeXI3vX0rFDuJVDzuR, I'm able to see the full filename of the image I upload; the documents in fl_files have a file field. I need to get this filename sent to my object so that I'm able to use the images in my UI.

Here's my progress:

Task:

struct Task{
    var string1:String
    var string2:String
    //var imageUpload:String
    var counter:Int
    var id: String

    var dictionary: [String: Any] {
        return [
            "string1": string1,
            "string2": string2,
            //"imageUpload": imageUpload,
            "counter": counter
        ]
    }
}

extension Task{
    init?(dictionary: [String : Any], id: String) {
        guard   let string1 = dictionary["string1"] as? String,
            let string2 = dictionary["string2"] as? String,
            //let imageUpload = dictionary["imageUpload"] as? String,
            let counter = dictionary["counter"] as? Int
            else { return nil }

        self.init(string1:string1, string2: string2, /*imageUpload: imageUpload,*/ counter: counter, id: id)
    }
}

VC:

private var documents: [DocumentSnapshot] = []
public var tasks: [Task] = []
private var listener : ListenerRegistration!

fileprivate func baseQuery() -> Query {
    return Firestore.firestore().collection("fl_content").limit(to: 50)
}

fileprivate var query: Query? {
    didSet {
        if let listener = listener {
            listener.remove()
        }
    }
}

override func viewDidLoad() {
    super.viewDidLoad()
    self.query = baseQuery()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    self.listener.remove()
}


override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    listenerStuff()
}

func listenerStuff(){
    self.listener =  query?.addSnapshotListener { (documents, error) in
        guard let snapshot = documents else {
            print("Error fetching documents results: \(error!)")
            return
        }

        let results = snapshot.documents.map { (document) -> Task in
            if let task = Task(dictionary: document.data(), id: document.documentID) {
                return task
            }
            else {
                fatalError("Unable to initialize type \(Task.self) with dictionary \(document.data())")
            }
        }

        self.tasks = results
        self.documents = snapshot.documents

        self.databaseTableView.reloadData()

    }
}

How do I query fl_files so that I can populate the imageUpload property of Task with the URL of the uploaded image? Do I do another separate query? Or can I access fl_files from baseQuery()?

EDIT

Here's my attempt at getting to fl_files from fl_content. Trying to just simply populate 2 text fields an an image field (in a UITableViewCell) from Firebase. Is property what I need in getdocument?

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "databaseCellID", for: indexPath) as! DatabaseTableViewCell
    let item = tasks[indexPath.row]

    cell.label1.text = item.string1
    cell.label2.text = item.string2
    let docRef = Firestore.firestore().collection("fl_content").document(item.id)
    print("PARENT \(docRef.parent)")
    docRef.getDocument { (document, error) in
        if let document = document, document.exists {
            let property = document.get("imageUpload")
            // TODO - use this property to get to fl_files?
            print("Document data: \(property!)")
        }
    }
}
Joe
  • 3,772
  • 3
  • 33
  • 64

2 Answers2

3

You will need to perform a separate query. There are no SQL-like join operations in Firestore, and references are not automatically followed.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Doug. I have a bounty here. Edited my question to include my progress in cellForRowAt. Need to "traverse" back out of fl_content into fl_files, to start a new query in getDocument. I don't know what to do after I've gotten document.get("imageUpload") – Joe Feb 03 '20 at 21:22
2

The simplest way to do this is probably to write a small Cloud Function that will respond to the uploaded files and place the image URL automatically in your desired collection, allowing for easier querying.

We will attach an onCreate listener to the fl_files collection, then write the downloadURL to the corresponding fl_content document when we see that a new file was created. Note that your actual content fields may be different from the examples I have used here (I'm not personally familiar with Flamelink).

/**
 * Firebase Cloud Function deployed to your same project.
 */

const functions = require('firebase-functions');
import { firestore } from 'firebase-admin';

exports.newFileUploaded = functions.firestore
  .document('fl_files/{newFileID}')
  .onCreate((snap, context) => { 

    const fileID = context.params.newFileID;
    const fileData = snap.data();

    // get whatever data you want out of fileData..
    // (let's assume there is a downloadURL property with the URL of the image)
    const downloadURL = fileData.downloadURL;

    // write that download URL to the corresponding fl_content document
    // (assuming the file ID is the same as the content ID)
    const contentRef = firestore().collection('fl_content').doc(fileID);

    // update the imageURL property, returning the Promise, so the function
    // does not terminate too early
    const updateData = { imageUpload: downloadURL };
    return contentRef.update(updateData);

});

Now, you are able to perform just 1 query to fl_content and the new file's image URL will be included in the request. The tradeoff we make is we have to perform 1 additional Firestore write to save 50% on all future read requests.

Alternatively, without a Cloud Function, we would have to perform 2 separate queries to get the content from fl_content and fl_files. As Doug mentioned, it is not possible to perform a JOIN-like query using Firestore due to the scalable way that it was designed.

Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45