Skip to content

Commit 0d9424e

Browse files
author
FalkWolsky
committed
Roundup Query Capability in Firebase Data Source
1 parent 5528883 commit 0d9424e

File tree

1 file changed

+101
-47
lines changed
  • server/node-service/src/plugins/firebase

1 file changed

+101
-47
lines changed

server/node-service/src/plugins/firebase/run.ts

Lines changed: 101 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,59 @@ import {
1111
import { DataSourceDataType } from "./dataSourceConfig";
1212
import { ActionDataType } from "./queryConfig";
1313

14+
function applyFieldFilter(
15+
query: FirebaseFirestore.Query<FirebaseFirestore.DocumentData>,
16+
fieldPath: string,
17+
operator: string,
18+
value: any
19+
): FirebaseFirestore.Query<FirebaseFirestore.DocumentData> {
20+
let firestoreOp: FirebaseFirestore.WhereFilterOp;
21+
switch (operator) {
22+
case "EQUAL": firestoreOp = "=="; break;
23+
case "GREATER_THAN": firestoreOp = ">"; break;
24+
case "LESS_THAN": firestoreOp = "<"; break;
25+
case "GREATER_THAN_OR_EQUAL": firestoreOp = ">="; break;
26+
case "LESS_THAN_OR_EQUAL": firestoreOp = "<="; break;
27+
case "ARRAY_CONTAINS": firestoreOp = "array-contains"; break;
28+
case "ARRAY_CONTAINS_ANY": firestoreOp = "array-contains-any"; break;
29+
default:
30+
throw badRequest(`Unsupported operator: ${operator}`);
31+
}
32+
33+
const actualValue = value.integerValue ?? value.stringValue ?? value.booleanValue ?? value.doubleValue;
34+
if (actualValue === undefined) {
35+
throw badRequest("Unsupported value type in structuredQuery");
36+
}
37+
38+
return query.where(fieldPath, firestoreOp, actualValue);
39+
}
40+
41+
// Helper function to apply a unary filter
42+
function applyUnaryFilter(
43+
query: FirebaseFirestore.Query<FirebaseFirestore.DocumentData>,
44+
fieldPath: string,
45+
operator: string
46+
): FirebaseFirestore.Query<FirebaseFirestore.DocumentData> {
47+
let firestoreOp: FirebaseFirestore.WhereFilterOp;
48+
switch (operator) {
49+
case "IS_NAN": firestoreOp = "=="; break;
50+
case "IS_NULL": firestoreOp = "=="; break;
51+
case "IS_NOT_NAN": firestoreOp = "!="; break;
52+
case "IS_NOT_NULL": firestoreOp = "!="; break;
53+
default:
54+
throw badRequest(`Unsupported unary operator: ${operator}`);
55+
}
56+
57+
return query.where(fieldPath, firestoreOp, null);
58+
}
59+
60+
// Helper function to extract cursor values
61+
function extractCursorValues(values: any[]): any[] {
62+
return values.map((v: { integerValue?: any; stringValue?: any; booleanValue?: any; doubleValue?: any; }) =>
63+
v.integerValue ?? v.stringValue ?? v.booleanValue ?? v.doubleValue ?? v.booleanValue
64+
).filter(value => value !== undefined);
65+
}
66+
1467
export async function runFirebasePlugin(
1568
actionData: ActionDataType,
1669
dataSourceConfig: DataSourceDataType
@@ -139,67 +192,69 @@ export async function runFirebasePlugin(
139192

140193
let query: FirebaseFirestore.Query<FirebaseFirestore.DocumentData> = ref;
141194

142-
// Apply `where` filters
143-
if (structuredQuery.where && structuredQuery.where.fieldFilter) {
144-
const fieldFilter = structuredQuery.where.fieldFilter;
145-
const fieldPath = fieldFilter.field?.fieldPath;
146-
const operator = fieldFilter.op;
147-
const value = fieldFilter.value;
195+
// Apply `select` fields projection if provided
196+
if (structuredQuery.select && structuredQuery.select.fields) {
197+
const selectedFields = structuredQuery.select.fields.map((field: { fieldPath: string }) => field.fieldPath);
198+
query = query.select(...selectedFields);
199+
}
148200

149-
if (!fieldPath || !operator || value === undefined) {
150-
throw badRequest("Invalid fieldFilter in where clause");
151-
}
201+
// Apply `where` filters
202+
if (structuredQuery.where) {
203+
if (structuredQuery.where.compositeFilter) {
204+
// Composite Filter (AND, OR)
205+
const compositeFilter = structuredQuery.where.compositeFilter;
206+
const filters = compositeFilter.filters;
207+
const operator = compositeFilter.op;
152208

153-
let firestoreOp: FirebaseFirestore.WhereFilterOp;
154-
switch (operator) {
155-
case "EQUAL":
156-
firestoreOp = "==";
157-
break;
158-
case "GREATER_THAN":
159-
firestoreOp = ">";
160-
break;
161-
case "LESS_THAN":
162-
firestoreOp = "<";
163-
break;
164-
case "GREATER_THAN_OR_EQUAL":
165-
firestoreOp = ">=";
166-
break;
167-
case "LESS_THAN_OR_EQUAL":
168-
firestoreOp = "<=";
169-
break;
170-
case "ARRAY_CONTAINS":
171-
firestoreOp = "array-contains";
172-
break;
173-
default:
174-
throw badRequest(`Unsupported operator: ${operator}`);
175-
}
209+
if (operator !== "AND") {
210+
throw badRequest("Only 'AND' composite filters are currently supported.");
211+
}
176212

177-
const actualValue = value.integerValue ?? value.stringValue ?? value.booleanValue ?? value.doubleValue;
178-
if (actualValue === undefined) {
179-
throw badRequest("Unsupported value type in structuredQuery");
213+
filters.forEach((filter: any) => {
214+
if (filter.fieldFilter) {
215+
const fieldFilter = filter.fieldFilter;
216+
const fieldPath = fieldFilter.field.fieldPath;
217+
const operator = fieldFilter.op;
218+
const value = fieldFilter.value;
219+
query = applyFieldFilter(query, fieldPath, operator, value);
220+
} else if (filter.unaryFilter) {
221+
const unaryFilter = filter.unaryFilter;
222+
const fieldPath = unaryFilter.field.fieldPath;
223+
const operator = unaryFilter.op;
224+
query = applyUnaryFilter(query, fieldPath, operator);
225+
}
226+
});
227+
} else if (structuredQuery.where.fieldFilter) {
228+
// Single Field Filter
229+
const fieldFilter = structuredQuery.where.fieldFilter;
230+
const fieldPath = fieldFilter.field.fieldPath;
231+
const operator = fieldFilter.op;
232+
const value = fieldFilter.value;
233+
query = applyFieldFilter(query, fieldPath, operator, value);
180234
}
181-
182-
query = query.where(fieldPath, firestoreOp, actualValue);
183235
}
236+
237+
// Get the total count using aggregate query before applying pagination
238+
const totalCount = await query.count().get().then((snapshot) => snapshot.data().count);
184239

185240
// Apply `orderBy`
186241
if (structuredQuery.orderBy && Array.isArray(structuredQuery.orderBy)) {
187-
for (const order of structuredQuery.orderBy) {
242+
structuredQuery.orderBy.forEach((order: { field: { fieldPath: string | FirebaseFirestore.FieldPath; }; direction: any; }) => {
188243
if (order.field && order.field.fieldPath) {
189244
query = query.orderBy(
190245
order.field.fieldPath,
191-
(order.direction || "asc") as FirebaseFirestore.OrderByDirection
246+
(order.direction || "asc").toLowerCase() as FirebaseFirestore.OrderByDirection
192247
);
193248
}
194-
}
249+
});
195250
}
196251

197252
// Apply `limit`
198253
if (structuredQuery.limit) {
199254
query = query.limit(structuredQuery.limit);
200255
}
201256

202-
// Apply `offset` (Firestore SDK doesn't support offset directly; simulate it with startAfter)
257+
// Apply `offset` (simulate it using startAfter since Firestore SDK doesn't support offset directly)
203258
if (structuredQuery.offset) {
204259
const offsetSnapshot = await query.limit(structuredQuery.offset).get();
205260
const lastVisible = offsetSnapshot.docs[offsetSnapshot.docs.length - 1];
@@ -208,28 +263,27 @@ export async function runFirebasePlugin(
208263
}
209264
}
210265

211-
// Apply `startAt` and `endAt` cursors, checking for undefined values
266+
// Apply `startAt` and `endAt` cursors
212267
if (structuredQuery.startAt && structuredQuery.startAt.values) {
213-
const startAtValues = structuredQuery.startAt.values.map((v: { integerValue: any; stringValue: any; booleanValue: any; doubleValue: any; }) => v.integerValue ?? v.stringValue ?? v.booleanValue ?? v.doubleValue).filter((value: any) => value !== undefined);
268+
const startAtValues = extractCursorValues(structuredQuery.startAt.values);
214269
if (startAtValues.length > 0) {
215270
query = query.startAt(...startAtValues);
216271
}
217272
}
218-
219273
if (structuredQuery.endAt && structuredQuery.endAt.values) {
220-
const endAtValues = structuredQuery.endAt.values.map((v: { integerValue: any; stringValue: any; booleanValue: any; doubleValue: any; }) => v.integerValue ?? v.stringValue ?? v.booleanValue ?? v.doubleValue).filter((value: any) => value !== undefined);
274+
const endAtValues = extractCursorValues(structuredQuery.endAt.values);
221275
if (endAtValues.length > 0) {
222276
query = query.endAt(...endAtValues);
223277
}
224278
}
225279

226280
// Execute the query
227281
const snapshot = await query.get();
228-
229282
if (snapshot.empty) {
230283
return [];
231284
}
232-
return snapshot.docs.map((doc) => doc.data());
285+
const documents = snapshot.empty ? [] : snapshot.docs.map((doc) => doc.data());
286+
return { totalCount, documents };
233287
});
234288
return data;
235289
}

0 commit comments

Comments
 (0)