diff --git a/packages/builder/src/components/backend/DatasourceNavigator/icons/Firebase.svelte b/packages/builder/src/components/backend/DatasourceNavigator/icons/Firebase.svelte new file mode 100644 index 0000000000..3a776a9217 --- /dev/null +++ b/packages/builder/src/components/backend/DatasourceNavigator/icons/Firebase.svelte @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/builder/src/components/backend/DatasourceNavigator/icons/firebase.svg b/packages/builder/src/components/backend/DatasourceNavigator/icons/firebase.svg new file mode 100644 index 0000000000..583b90f5c3 --- /dev/null +++ b/packages/builder/src/components/backend/DatasourceNavigator/icons/firebase.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js b/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js index 350fccf73f..515f20a93b 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js +++ b/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js @@ -12,6 +12,7 @@ import Rest from "./Rest.svelte" import Budibase from "./Budibase.svelte" import Oracle from "./Oracle.svelte" import GoogleSheets from "./GoogleSheets.svelte" +import Firebase from "./Firebase.svelte" export default { BUDIBASE: Budibase, @@ -28,4 +29,5 @@ export default { REST: Rest, ORACLE: Oracle, GOOGLE_SHEETS: GoogleSheets, + FIREBASE: Firebase, } diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index 4d6a7e3884..9a623241ca 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -178,6 +178,7 @@ export const IntegrationTypes = { ORACLE: "ORACLE", INTERNAL: "INTERNAL", GOOGLE_SHEETS: "GOOGLE_SHEETS", + FIREBASE: "FIREBASE", } export const IntegrationNames = { @@ -195,6 +196,7 @@ export const IntegrationNames = { [IntegrationTypes.ORACLE]: "Oracle", [IntegrationTypes.INTERNAL]: "Internal", [IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets", + [IntegrationTypes.FIREBASE]: "Firebase", } export const SchemaTypeOptions = [ diff --git a/packages/server/package.json b/packages/server/package.json index 484ffef85f..df5b4262eb 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -77,6 +77,7 @@ "@bull-board/api": "^3.7.0", "@bull-board/koa": "^3.7.0", "@elastic/elasticsearch": "7.10.0", + "@google-cloud/firestore": "^5.0.2", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", "@sentry/node": "^6.0.0", diff --git a/packages/server/src/definitions/datasource.ts b/packages/server/src/definitions/datasource.ts index 2e2ad25f58..88115237a0 100644 --- a/packages/server/src/definitions/datasource.ts +++ b/packages/server/src/definitions/datasource.ts @@ -48,6 +48,7 @@ export enum SourceNames { REST = "REST", ORACLE = "ORACLE", GOOGLE_SHEETS = "GOOGLE_SHEETS", + FIREBASE = "FIREBASE", } export enum IncludeRelationships { diff --git a/packages/server/src/integrations/firebase.ts b/packages/server/src/integrations/firebase.ts new file mode 100644 index 0000000000..503dae5c95 --- /dev/null +++ b/packages/server/src/integrations/firebase.ts @@ -0,0 +1,205 @@ +import { + DatasourceFieldTypes, + Integration, + QueryTypes, +} from "../definitions/datasource" +import { IntegrationBase } from "./base/IntegrationBase" +import { Firestore, WhereFilterOp } from "@google-cloud/firestore" + +module Firebase { + interface FirebaseConfig { + email: string + privateKey: string + projectId: string + serviceAccount?: string + } + + const SCHEMA: Integration = { + docs: "https://firebase.google.com/docs/firestore/quickstart", + friendlyName: "Firestore", + description: + "Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.", + datasource: { + email: { + type: DatasourceFieldTypes.STRING, + required: true, + }, + privateKey: { + type: DatasourceFieldTypes.STRING, + required: true, + }, + projectId: { + type: DatasourceFieldTypes.STRING, + required: true, + }, + serviceAccount: { + type: DatasourceFieldTypes.JSON, + required: false, + }, + }, + query: { + create: { + type: QueryTypes.JSON, + }, + read: { + type: QueryTypes.JSON, + }, + update: { + type: QueryTypes.JSON, + }, + delete: { + type: QueryTypes.JSON, + }, + }, + extra: { + collection: { + displayName: "Collection", + type: DatasourceFieldTypes.STRING, + required: true, + }, + filterField: { + displayName: "Filter field", + type: DatasourceFieldTypes.STRING, + required: false, + }, + filter: { + displayName: "Filter comparison", + type: DatasourceFieldTypes.LIST, + required: false, + data: { + read: [ + "==", + "<", + "<=", + "==", + "!=", + ">=", + ">", + "array-contains", + "in", + "not-in", + "array-contains-any", + ], + }, + }, + filterValue: { + displayName: "Filter value", + type: DatasourceFieldTypes.STRING, + required: false, + }, + }, + } + + class FirebaseIntegration implements IntegrationBase { + private config: FirebaseConfig + private db: Firestore + + constructor(config: FirebaseConfig) { + this.config = config + if (config.serviceAccount) { + const serviceAccount = JSON.parse(config.serviceAccount) + this.db = new Firestore({ + projectId: serviceAccount.project_id, + credentials: { + client_email: serviceAccount.client_email, + private_key: serviceAccount.private_key, + }, + }) + } else { + this.db = new Firestore({ + projectId: config.projectId, + credentials: { + client_email: config.email, + private_key: config.privateKey, + }, + }) + } + } + + async create(query: { json: object; extra: { [key: string]: string } }) { + try { + const documentReference = this.db + .collection(query.extra.collection) + .doc() + await documentReference.set({ ...query.json, id: documentReference.id }) + const snapshot = await documentReference.get() + return snapshot.data() + } catch (err) { + console.error("Error writing to Firestore", err) + throw err + } + } + + async read(query: { json: object; extra: { [key: string]: string } }) { + try { + let snapshot + const collectionRef = this.db.collection(query.extra.collection) + if ( + query.extra.filterField && + query.extra.filter && + query.extra.filterValue + ) { + snapshot = await collectionRef + .where( + query.extra.filterField, + query.extra.filter as WhereFilterOp, + query.extra.filterValue + ) + .get() + } else { + snapshot = await collectionRef.get() + } + const result: any[] = [] + snapshot.forEach(doc => result.push(doc.data())) + + return result + } catch (err) { + console.error("Error querying Firestore", err) + throw err + } + } + + async update(query: { + json: Record + extra: { [key: string]: string } + }) { + try { + await this.db + .collection(query.extra.collection) + .doc(query.json.id) + .update(query.json) + + return ( + await this.db + .collection(query.extra.collection) + .doc(query.json.id) + .get() + ).data() + } catch (err) { + console.error("Error writing to firebase", err) + throw err + } + } + + async delete(query: { + json: { id: string } + extra: { [key: string]: string } + }) { + try { + await this.db + .collection(query.extra.collection) + .doc(query.json.id) + .delete() + return true + } catch (err) { + console.error("Error writing to mongodb", err) + throw err + } + } + } + + module.exports = { + schema: SCHEMA, + integration: FirebaseIntegration, + } +} diff --git a/packages/server/src/integrations/index.ts b/packages/server/src/integrations/index.ts index 00b00c25fb..07f3211fcb 100644 --- a/packages/server/src/integrations/index.ts +++ b/packages/server/src/integrations/index.ts @@ -10,6 +10,7 @@ const mysql = require("./mysql") const arangodb = require("./arangodb") const rest = require("./rest") const googlesheets = require("./googlesheets") +const firebase = require("./firebase") const { SourceNames } = require("../definitions/datasource") const environment = require("../environment") @@ -25,6 +26,7 @@ const DEFINITIONS = { [SourceNames.MYSQL]: mysql.schema, [SourceNames.ARANGODB]: arangodb.schema, [SourceNames.REST]: rest.schema, + [SourceNames.FIREBASE]: firebase.schema, } const INTEGRATIONS = { @@ -39,6 +41,7 @@ const INTEGRATIONS = { [SourceNames.MYSQL]: mysql.integration, [SourceNames.ARANGODB]: arangodb.integration, [SourceNames.REST]: rest.integration, + [SourceNames.FIREBASE]: firebase.integration, } // optionally add oracle integration if the oracle binary can be installed