Skip to content

Commit 03b2d94

Browse files
authored
Merge pull request lowcoder-org#250 from sarike/ds-firebase
firebase datasource
2 parents 820da4a + 054508a commit 03b2d94

File tree

15 files changed

+1583
-16
lines changed

15 files changed

+1583
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.idea/
22
logs/
33
client/.yarn/cache/*.zip
4+
server/node-service/.yarn/cache/*.zip

server/node-service/.prettierrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"printWidth": 100
3+
}

server/node-service/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
module.exports = {
33
preset: "ts-jest",
44
testEnvironment: "node",
5-
testTimeout: 60000,
5+
testTimeout: 10 * 60000,
66
testPathIgnorePatterns: ["/node_modules/", "/build/"],
77
};

server/node-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"dynamodb-data-types": "^4.0.1",
3636
"express": "^4.18.2",
3737
"express-async-errors": "^3.1.1",
38+
"firebase-admin": "^11.5.0",
3839
"jsonpath": "^1.1.1",
3940
"lodash": "^4.17.21",
4041
"loglevel": "^1.8.1",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ConfigToType } from "openblocks-sdk/dataSource";
2+
3+
const dataSourceConfig = {
4+
type: "dataSource",
5+
params: [
6+
{
7+
key: "databaseUrl",
8+
label: "Firebase Database URL",
9+
tooltip:
10+
"You can find your database URL and Firestore ID in your [Firebase project console](https://console.firebase.google.com/)",
11+
type: "textInput",
12+
},
13+
{
14+
key: "firestoreId",
15+
label: "Firestore Project ID",
16+
type: "textInput",
17+
},
18+
{
19+
key: "privateKey",
20+
label: "Private Key",
21+
type: "password",
22+
tooltip:
23+
"The [document](https://firebase.google.com/docs/admin/setup) on how to obtain the private key.",
24+
},
25+
],
26+
} as const;
27+
28+
export type DataSourceDataType = ConfigToType<typeof dataSourceConfig>;
29+
30+
export default dataSourceConfig;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { DataSourcePlugin } from "openblocks-sdk/dataSource";
2+
import dataSourceConfig, { DataSourceDataType } from "./dataSourceConfig";
3+
import queryConfig, { ActionDataType } from "./queryConfig";
4+
import { runFirebasePlugin } from "./run";
5+
6+
const firebasePlugin: DataSourcePlugin<ActionDataType, DataSourceDataType> = {
7+
id: "firebase",
8+
icon: "firebase.svg",
9+
name: "Firebase",
10+
category: "api",
11+
queryConfig,
12+
dataSourceConfig,
13+
run: function (actionData, dataSourceConfig): Promise<any> {
14+
return runFirebasePlugin(actionData, dataSourceConfig);
15+
},
16+
};
17+
18+
export default firebasePlugin;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { ConfigToType, QueryConfig } from "openblocks-sdk/dataSource";
2+
3+
enum FirebaseCategory {
4+
RealtimeDatabase = "RealtimeDatabase",
5+
Firestore = "Firestore",
6+
}
7+
8+
const databaseRefParamConfig = {
9+
key: "databaseRef",
10+
label: "Database Ref",
11+
type: "textInput",
12+
} as const;
13+
14+
const dataParamConfig = {
15+
key: "data",
16+
label: "Data",
17+
type: "jsonInput",
18+
} as const;
19+
20+
const firestoreCollectionParamConfig = {
21+
key: "collection",
22+
label: "Collection",
23+
type: "textInput",
24+
} as const;
25+
26+
const firestoreDocIdParamConfig = {
27+
key: "documentId",
28+
label: "Document ID",
29+
type: "textInput",
30+
} as const;
31+
32+
const firestoreParentDocIdParamConfig = {
33+
key: "parentDocumentId",
34+
label: "Parent",
35+
type: "textInput",
36+
tooltip:
37+
"The parent document id of collections you want to list. Leave empty for top-level collections.",
38+
} as const;
39+
40+
const firestoreDataParamConfig = {
41+
key: "data",
42+
label: "Data",
43+
type: "jsonInput",
44+
} as const;
45+
46+
const categories = {
47+
label: "Service",
48+
items: [
49+
{ label: "Realtime Database", value: FirebaseCategory.RealtimeDatabase },
50+
{ label: "Firestore", value: FirebaseCategory.Firestore },
51+
],
52+
};
53+
54+
const queryConfig = {
55+
type: "query",
56+
categories,
57+
label: "Action",
58+
actions: [
59+
// actions of realtime database
60+
...(
61+
[
62+
{
63+
label: "Query Database",
64+
actionName: "RTDB.QueryDatabase",
65+
params: [databaseRefParamConfig],
66+
},
67+
{
68+
label: "Set Data",
69+
actionName: "RTDB.SetData",
70+
params: [databaseRefParamConfig, dataParamConfig],
71+
},
72+
{
73+
label: "Update Data",
74+
actionName: "RTDB.UpdateData",
75+
params: [databaseRefParamConfig, dataParamConfig],
76+
},
77+
{
78+
label: "Append Data to a list",
79+
actionName: "RTDB.AppendDataToList",
80+
params: [databaseRefParamConfig, dataParamConfig],
81+
},
82+
] as const
83+
).map((i) => ({ ...i, category: [FirebaseCategory.RealtimeDatabase] })),
84+
85+
// actions of firestore
86+
...(
87+
[
88+
{
89+
label: "Query Firestore",
90+
actionName: "FS.QueryFireStore",
91+
params: [
92+
firestoreCollectionParamConfig,
93+
{
94+
key: "orderBy",
95+
label: "Order by",
96+
type: "textInput",
97+
},
98+
{
99+
key: "orderDirection",
100+
label: "Order direction",
101+
type: "textInput",
102+
defaultValue: "asc",
103+
placeholder: "asc",
104+
},
105+
{
106+
key: "limit",
107+
label: "Limit",
108+
type: "numberInput",
109+
defaultValue: 10,
110+
},
111+
],
112+
},
113+
{
114+
label: "Insert Document",
115+
actionName: "FS.InsertDocument",
116+
params: [
117+
firestoreCollectionParamConfig,
118+
{
119+
...firestoreDocIdParamConfig,
120+
tooltip: "Leaving empty will use auto generated document id.",
121+
},
122+
firestoreDataParamConfig,
123+
],
124+
},
125+
{
126+
label: "Update Document",
127+
actionName: "FS.UpdateDocument",
128+
params: [
129+
firestoreCollectionParamConfig,
130+
firestoreDocIdParamConfig,
131+
firestoreDataParamConfig,
132+
],
133+
},
134+
{
135+
label: "Get Document",
136+
actionName: "FS.GetDocument",
137+
params: [firestoreCollectionParamConfig, firestoreDocIdParamConfig],
138+
},
139+
{
140+
label: "Delete Document",
141+
actionName: "FS.DeleteDocument",
142+
params: [firestoreCollectionParamConfig, firestoreDocIdParamConfig],
143+
},
144+
{
145+
label: "Get Collections",
146+
actionName: "FS.GetCollections",
147+
params: [firestoreParentDocIdParamConfig],
148+
},
149+
] as const
150+
).map((i) => ({ ...i, category: [FirebaseCategory.Firestore] })),
151+
],
152+
} as const;
153+
154+
export type ActionDataType = ConfigToType<typeof queryConfig>;
155+
156+
export default queryConfig;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { runFirebasePlugin } from "./run";
2+
3+
const privateKey = process.env["GOOGLE_PRIVATE_KEY"] || "";
4+
5+
test("realtime database", async () => {
6+
const res = await runFirebasePlugin(
7+
{ actionName: "RTDB.QueryDatabase", databaseRef: "/hello" },
8+
{
9+
databaseUrl: "https://sarike-a3de9-default-rtdb.asia-southeast1.firebasedatabase.app/",
10+
privateKey,
11+
firestoreId: "",
12+
}
13+
);
14+
console.info(res);
15+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { badRequest } from "../../common/error";
2+
import { initializeApp, deleteApp, cert } from "firebase-admin/app";
3+
import { getDatabase, Reference } from "firebase-admin/database";
4+
import {
5+
CollectionReference,
6+
DocumentReference,
7+
getFirestore,
8+
OrderByDirection,
9+
} from "firebase-admin/firestore";
10+
import { DataSourceDataType } from "./dataSourceConfig";
11+
import { ActionDataType } from "./queryConfig";
12+
13+
export async function runFirebasePlugin(
14+
actionData: ActionDataType,
15+
dataSourceConfig: DataSourceDataType
16+
) {
17+
const { actionName } = actionData;
18+
const { privateKey, databaseUrl } = dataSourceConfig;
19+
const serviceAccount = JSON.parse(privateKey);
20+
21+
const app = initializeApp({
22+
credential: cert(serviceAccount),
23+
databaseURL: databaseUrl,
24+
});
25+
26+
const witDbRef = <T>(fn: (ref: Reference) => T): T => {
27+
if (!("databaseRef" in actionData)) {
28+
throw badRequest("not a realtime database action:" + actionName);
29+
}
30+
const ref = getDatabase().ref(actionData.databaseRef);
31+
return fn(ref);
32+
};
33+
34+
const withFirestoreCollection = <T>(fn: (ref: CollectionReference) => T): T => {
35+
if (!("collection" in actionData)) {
36+
throw badRequest("not a firestore action with collection:" + actionName);
37+
}
38+
const ref = getFirestore().collection(actionData.collection);
39+
return fn(ref);
40+
};
41+
42+
const withFirestoreDoc = <T>(fn: (ref: DocumentReference) => T): T => {
43+
if (!("collection" in actionData) || !("documentId" in actionData)) {
44+
throw badRequest("not a firestore action with collection and documentId:" + actionName);
45+
}
46+
const ref = getFirestore().collection(actionData.collection).doc(actionData.documentId);
47+
return fn(ref);
48+
};
49+
50+
const successResult = { success: true };
51+
52+
try {
53+
// firebase
54+
if (actionName === "RTDB.QueryDatabase") {
55+
const data = await witDbRef((ref) => ref.once("value"));
56+
return data.val();
57+
}
58+
59+
if (actionName === "RTDB.SetData") {
60+
await witDbRef((ref) => ref.set(actionData.data));
61+
return successResult;
62+
}
63+
64+
if (actionName === "RTDB.UpdateData") {
65+
await witDbRef((ref) => ref.update(actionData.data));
66+
return successResult;
67+
}
68+
69+
if (actionName === "RTDB.AppendDataToList") {
70+
await witDbRef((ref) => ref.push(actionData.data));
71+
return successResult;
72+
}
73+
74+
// firebase
75+
if (actionName === "FS.GetCollections") {
76+
let collections;
77+
if (actionData.parentDocumentId) {
78+
collections = await getFirestore().doc(actionData.parentDocumentId).listCollections();
79+
} else {
80+
collections = await getFirestore().listCollections();
81+
}
82+
return collections.map((i) => i.id);
83+
}
84+
85+
if (actionName === "FS.QueryFireStore") {
86+
const data = await withFirestoreCollection(async (ref) => {
87+
let query;
88+
if (actionData.orderBy) {
89+
query = ref.orderBy(
90+
actionData.orderBy,
91+
(actionData.orderDirection || "asc") as OrderByDirection
92+
);
93+
}
94+
if (actionData.limit > 0) {
95+
query = (query || ref).limit(actionData.limit);
96+
}
97+
const snapshot = await (query || ref).get();
98+
if (snapshot.empty) {
99+
return [];
100+
}
101+
return snapshot.docs.map((i) => i.data());
102+
});
103+
return data;
104+
}
105+
106+
if (actionName === "FS.GetDocument") {
107+
return await withFirestoreDoc(async (ref) => (await ref.get()).data());
108+
}
109+
110+
if (actionName === "FS.InsertDocument") {
111+
return await withFirestoreCollection(async (ref) => {
112+
if (actionData.documentId) {
113+
await ref.doc(actionData.documentId).set(actionData.data);
114+
} else {
115+
await ref.add(actionData.data);
116+
}
117+
return successResult;
118+
});
119+
}
120+
121+
if (actionName === "FS.UpdateDocument") {
122+
return await withFirestoreDoc(async (ref) => {
123+
await ref.update(actionData.data);
124+
return successResult;
125+
});
126+
}
127+
128+
if (actionName === "FS.DeleteDocument") {
129+
return await withFirestoreDoc(async (ref) => {
130+
await ref.delete();
131+
return successResult;
132+
});
133+
}
134+
} finally {
135+
deleteApp(app);
136+
}
137+
}

0 commit comments

Comments
 (0)