UNPKG

@discue/paddle-integration-firestore

Version:

Paddle payments integration for Google Cloud Firestore

359 lines (320 loc) 12 kB
const { FieldValue, getFirestore } = require('firebase-admin/firestore') const initFirebase = require('./init-firebase') const app = initFirebase() const firestore = getFirestore(app) module.exports = { /** * @typedef {import('@google-cloud/firestore').CollectionReference} CollectionReference */ /** * @typedef {import('@google-cloud/firestore').QuerySnapshot} QuerySnapshot */ /** * @typedef {import('@google-cloud/firestore').DocumentReference} DocumentReference */ /** * @typedef {import('@google-cloud/firestore').Query} Query */ /** * Get a reference to a Firestore collection * * @param {String} name the collection name * @returns {CollectionReference} */ _getCollection(name) { return firestore.collection(name) }, /** * Get all Refs (name & id properties) of a collection in a nested array * * @param {CollectionReference|Query} target the firestore collection reference or query to execute * @param {String} resultName the name of the target resources array * @returns {Promise<Object>} */ async _getAllDocumentRefs(target, resultName) { const snapshot = await target.get() const result = {} result[resultName] = [] snapshot.forEach((doc) => { const { id } = doc const { name } = doc.data() result[resultName].push({ id, name }) }) return result }, /** * Get all Docs of a collection in a nested array * * @param {CollectionReference} collection the firestore collection reference * @param {String} resultName the name of the target resources array * @param {boolean} [addMetadata=false] true if metadata should be returned * @returns {Promise<Object>} */ async _getAllDocuments(collection, resultName, addMetadata) { const snapshot = await collection.get() return this._snapshotToData(snapshot, resultName, addMetadata) }, /** * Get all docs of a snapshot in a nested array * * @param {QuerySnapshot} snapshot the firestore snapshot reference * @param {String} resultName the name of the target resources array * @param {boolean} [addMetadata=false] true if metadata should be returned * @returns {Promise<Object>} */ async _snapshotToData(snapshot, resultName, addMetadata) { const result = {} result[resultName] = [] snapshot.forEach((doc) => { const data = doc.data() if (!addMetadata) { this._deleteMetadata(data) } else { // add timestamps doc.createTime && (data._meta_data.created_at = doc.createTime) doc.updateTime && (data._meta_data.updated_at = doc.updateTime) data._meta_data.last_read_at = doc.readTime } result[resultName].push(data) }) return result }, _deleteMetadata(data) { Object.keys(data).forEach(key => { if (key.startsWith('_')) { delete data[key] } }) return data }, /** * Get a Document by id * * @param {String|CollectionReference} collection the collection name * @param {any} byId the document id * @returns {Promise.<DocumentReference>} */ async _getDocument(collection, byId) { if (typeof collection === 'string') { return firestore.collection(collection).doc(byId) } else { return collection.doc(byId) } }, /** * Get a document that matches a specific condition / operator * * @param {CollectionReference|string} collection the target collection or target collection name (if not a nested one) * @param {String} resultName the name of the result field * @param {String} field the fieldname * @param {String} operator the equality operator. See also https://firebase.google.com/docs/firestore/query-data/queries#query_operators * @param {String} target the operation target value * @param {boolean} [addMetadata=false] true if metadata should be returned * @returns */ async _getDocumentsWhere(collection, resultName, field, operator, target, addMetadata) { return this._getDocumentsWhereMultiple(collection, resultName, [{ field, operator, target }], null, addMetadata) }, /** * Get a document that matches a specific set of condition / operators * * @param {CollectionReference|string} collection the target collection or target collection name (if not a nested one) * @param {String} resultName the name of the result field * @param {Array} queries an array containing queries in format {field, operator, target} * @param {boolean} [addMetadata=false] true if metadata should be returned * @returns */ async _getDocumentsWhereMultiple(collection, resultName, queries, sortBy, addMetadata) { let collectionRef if (typeof collection === 'string') { collectionRef = firestore.collection(collection) } else { collectionRef = collection } let query = queries.reduce((queryOrCollection, nextQueryFunction) => { return nextQueryFunction(queryOrCollection) }, collectionRef) if (sortBy) { query = query.orderBy(sortBy, 'desc') } const snapshot = await query.get() return this._snapshotToData(snapshot, resultName, addMetadata) }, /** * Insert a document into a collection * * @param {CollectionReference} collection * @param {any} id the document id * @param {Object} resource the document to add to the collection * @param {Object} additionalMetadata additional metadata to add to the document */ async _putDocument(collection, id, resource, additionalMetadata) { const metadata = this._createMetadata(additionalMetadata) await collection.doc(id).set(Object.assign(resource, { id }, metadata)) }, /** * Updates a document with given properties * * @param {CollectionReference} collection * @param {any} id * @param {Object} update * @throws 'Not Found' if no document with id exists */ async _updateDocument(collection, id, update) { const document = await collection.doc(id) // make sure document exists before updating await this._getDataIfDocExists(document) return this._executeDocumentUpdate(document, update) }, /** * Update given document with given properties * * @param {DocumentReference} document * @param {Object} update */ async _executeDocumentUpdate(document, update) { const metadata = this._createMetadataForUpdate() await document.update(Object.assign(update, metadata)) }, /** * @typedef MetaDataForUpdate * @property updated_at */ /** * Create a MetaData object for updates * * @returns {MetaDataForUpdate} */ _createMetadataForUpdate() { // need to use dot notation to enable update of nested value // https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects return { '_meta_data.updated_at': FieldValue.serverTimestamp() } }, /** * @typedef MetaData * @property created_at * @property updated_at */ /** * Create a MetaData object for a new object * @param {Object} additionalMetadata additional metadata to add * * @returns {MetaData} */ _createMetadata(additionalMetadata) { const metadata = { _meta_data: { created_at: FieldValue.serverTimestamp(), updated_at: FieldValue.serverTimestamp() } } Object.entries(additionalMetadata).forEach(([key, value]) => { metadata._meta_data[key] = value }) return metadata }, /** * Get data of a document if document exists * * @param {DocumentReference} doc the document reference * @param {boolean} [addMetadata=false] true if metadata should be returned * @throws 'Not Found' if document's exists property is false * @returns {Promise<any>} data of the document */ async _getDataIfDocExists(doc, addMetadata = false) { const result = await doc.get() if (result.exists === false || result.empty === true) { throw new Error(module.exports.ERROR_NOT_FOUND) } let data let snapshot if (result.forEach) { snapshot = result.docs[0] } else { snapshot = result } data = snapshot.data() if (!addMetadata) { this._deleteMetadata(data) } else { // add timestamps result.createTime && (data._meta_data.created_at = result.createTime) result.updateTime && (data._meta_data.updated_at = result.updateTime) data._meta_data.last_read_at = result.readTime } return data }, /** * Verify a document with given name and id does not exists * * @param {CollectionReference} collection the firestore collection reference * @param {String|number} id the id of the another resource * @param {String} [name] optional, the name of another resource * @throws 'Exists' if a resource with name or id exists * @returns {Promise} */ async _verifyIsNotADuplicate(collection, id, name) { let snapshot = await collection.doc(id).get() if (snapshot.exists) { throw new Error(module.exports.ERROR_EXISTS) } if (name) { snapshot = await collection.where('name', '==', name).get() if (!snapshot.empty) { throw new Error(module.exports.ERROR_EXISTS) } } }, /** * Return a sub collection of a document of another collection * * @param {String} parentCollectionName the name of the parent collection * @param {String|number} parentId the id of the parent document * @param {String} nestedCollectionName the name of the nested collection * @returns {Promise<CollectionReference>} */ async _getNestedCollectionForParentDocument(parentCollectionName, parentId, nestedCollectionName) { const doc = await this._getDocument(parentCollectionName, parentId) await this._getDataIfDocExists(doc) return doc.collection(nestedCollectionName) }, /** * Deletes all subcollections of a collection * * @param {String} parentCollectionName * @param {String|number} parentId */ async _deleteSubCollections(parentCollectionName, parentId) { const doc = await this._getDocument(parentCollectionName, parentId) await this._getDataIfDocExists(doc) await firestore.recursiveDelete(doc) }, /** * Executes the given query until snapshot size is zero * * @param {Query} query the query to execute * @param {Function} callback the callback will be called on completion (no more batches to process) * @returns */ async _executeBatch(query, callback) { const snapshot = await query.get() const batchSize = snapshot.size if (batchSize === 0) { callback() return } const batch = firestore.batch() snapshot.docs.forEach((doc) => { batch.delete(doc.ref) }) await batch.commit() // eslint-disable-next-line no-undef process.nextTick(() => { this._executeBatch(query, callback) }) } } module.exports.ERROR_EXISTS = 'Exists' module.exports.ERROR_NOT_FOUND = 'Not Found'