Skip to content

Commit d56c917

Browse files
authored
Proxy data through Cloud Functions (#108)
1 parent 3d25687 commit d56c917

13 files changed

+306
-127
lines changed

app/package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@fortawesome/vue-fontawesome": "^2.0.2",
1818
"core-js": "^3.6.5",
1919
"firebase": "^8.2.4",
20+
"lodash.get": "^4.4.2",
2021
"vue": "^2.6.11",
2122
"vue-class-component": "^7.2.3",
2223
"vue-gtag": "^1.11.0",

app/src/components/HeaderSidebarLayout.vue

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="relative lg:grid lg:grid-cols-12 gap-0">
2+
<div class="relative fill-height lg:grid lg:grid-cols-12 gap-0">
33
<!-- Side bar -->
44
<div class="desktop-only lg:col-span-2 bg-gray-50 py-10 px-10">
55
<slot name="sidebar"></slot>
@@ -28,4 +28,8 @@ import { Component, Vue } from "vue-property-decorator";
2828
export default class HeaderSidebarLayout extends Vue {}
2929
</script>
3030

31-
<style scoped lang="postcss"></style>
31+
<style scoped lang="postcss">
32+
.fill-height {
33+
min-height: 100vh;
34+
}
35+
</style>

app/src/plugins/data.ts

+96-53
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,61 @@
11
import { BlogData, RepoData, RepoPage } from "../../../shared/types";
2-
import { firestore } from "./firebase";
32

4-
import firebase from "firebase";
3+
import {
4+
FirestoreQuery,
5+
QueryResult,
6+
QueryResultDocument,
7+
} from "../../../shared/types/FirestoreQuery";
8+
9+
// eslint-disable-next-line
10+
const lodashGet = require("lodash.get");
511

612
export interface PagedResponse<T> {
7-
q: firebase.firestore.Query<T>;
13+
collectionPath: string;
14+
q: FirestoreQuery;
815
perPage: number;
916
pages: T[][];
1017
currentPage: number;
1118
hasNext: boolean;
12-
lastDoc: firebase.firestore.QueryDocumentSnapshot | null;
19+
lastDoc: QueryResultDocument<T> | null;
1320
}
1421

15-
export async function getDocs<T>(query: firebase.firestore.Query<T>) {
16-
const snap = await query.get();
17-
18-
const snapshots = snap.docs;
19-
const data = snap.docs.map((d) => d.data());
20-
return { snapshots, data };
22+
function getApiHost(): string {
23+
// In development the hosting emulator runs at port 5000
24+
// while the Vue dev server runs elsewhere. In prod this is
25+
// not an issue
26+
return window.location.hostname === "localhost"
27+
? `http://localhost:5000`
28+
: "";
2129
}
2230

23-
export function reposRef(product: string) {
24-
return firestore()
25-
.collection("products")
26-
.doc(product)
27-
.collection("repos")
28-
.withConverter({
29-
toFirestore: (obj: RepoData) => obj,
30-
fromFirestore: (snap) => snap.data() as RepoData,
31-
});
32-
}
31+
async function fetchDoc(docPath: string) {
32+
const params = new URLSearchParams({
33+
path: docPath,
34+
});
3335

34-
export function reposQuery(product: string) {
35-
return reposRef(product) as firebase.firestore.Query<RepoData>;
36+
const res = await fetch(`${getApiHost()}/api/docProxy?${params.toString()}`);
37+
return await res.json();
3638
}
3739

38-
export function blogsRef(product: string) {
39-
return firestore()
40-
.collection("products")
41-
.doc(product)
42-
.collection("blogs")
43-
.withConverter({
44-
toFirestore: (obj: BlogData) => obj,
45-
fromFirestore: (snap) => snap.data() as BlogData,
46-
});
47-
}
40+
async function fetchQuery(collectionPath: string, q: FirestoreQuery) {
41+
const params = new URLSearchParams({
42+
path: collectionPath,
43+
q: btoa(JSON.stringify(q)),
44+
});
4845

49-
export function blogsQuery(product: string) {
50-
return blogsRef(product) as firebase.firestore.Query<BlogData>;
46+
const res = await fetch(
47+
`${getApiHost()}/api/queryProxy?${params.toString()}`
48+
);
49+
return await res.json();
5150
}
5251

5352
export function emptyPageResponse<T>(
54-
q: firebase.firestore.Query<T>,
53+
collectionPath: string,
54+
q: FirestoreQuery,
5555
perPage: number
5656
): PagedResponse<T> {
5757
return {
58+
collectionPath,
5859
q,
5960
perPage,
6061
pages: [],
@@ -69,38 +70,60 @@ export async function prevPage<T>(res: PagedResponse<T>) {
6970
}
7071

7172
export async function nextPage<T>(res: PagedResponse<T>) {
72-
// Fetch the next page based on the limit and startAfter
73-
let q = res.q;
74-
if (res.lastDoc) {
75-
q = q.startAfter(res.lastDoc);
73+
// TODO: Make a proper deep copy
74+
const q: FirestoreQuery = JSON.parse(JSON.stringify(res.q));
75+
76+
// Add orderBy
77+
q.orderBy = q.orderBy || [];
78+
79+
// Add the startAfter clauses
80+
if (res.lastDoc != null) {
81+
// Add one startAfter per orderBy
82+
// eslint-disable-next-line
83+
const startAfter: any[] = [];
84+
for (const ob of q.orderBy) {
85+
const fieldVal = lodashGet(res.lastDoc.data, ob.fieldPath);
86+
startAfter.push(fieldVal);
87+
}
88+
89+
// Add a secondary order on name
90+
q.orderBy.push({
91+
fieldPath: "__name__",
92+
direction: "desc",
93+
});
94+
startAfter.push(res.lastDoc.id);
95+
96+
q.startAfter = startAfter;
7697
}
7798

7899
// Load one more than the limit to see if we have anything more
79100
// beyond the minimum
80-
q = q.limit(res.perPage + 1);
101+
q.limit = res.perPage + 1;
81102

82-
const { data, snapshots } = await getDocs(q);
103+
const json = await fetchQuery(res.collectionPath, q);
104+
const { docs } = json as QueryResult<T>;
83105

84106
// If we were able to find more than the per-page minimum, there
85107
// is still more after this
86-
res.hasNext = data.length > res.perPage;
108+
res.hasNext = docs.length > res.perPage;
87109

88110
if (res.hasNext) {
89111
// If there are more pages after this then we need to
90112
// chop off the extra one we loaded (see above) and then add the data
91-
const lastDoc = snapshots[snapshots.length - 2];
92-
const pageData = data.slice(0, data.length - 1);
113+
const lastDoc = docs[docs.length - 2];
114+
const pageData = docs.slice(0, docs.length - 1).map((d) => d.data);
93115

94116
res.lastDoc = lastDoc;
95117
res.pages.push(pageData);
96118
} else {
97119
// If this is the last page, just add the data (if it exists)
98120
// and accept the last snapshot
99-
if (data.length > 0) {
100-
res.pages.push(data);
121+
if (docs.length > 0) {
122+
const pageData = docs.map((d) => d.data);
123+
res.pages.push(pageData);
101124
}
102125

103-
res.lastDoc = snapshots[snapshots.length - 1];
126+
res.lastDoc = docs[docs.length - 1];
104127
}
105128

106129
// Finally increment the page
@@ -111,19 +134,39 @@ export async function fetchRepo(
111134
product: string,
112135
id: string
113136
): Promise<RepoData> {
114-
const ref = reposRef(product).doc(id);
137+
const repoPath = `/products/${product}/repos/${id}`;
138+
const json = await fetchDoc(repoPath);
115139

116-
const snap = await ref.get();
117-
return snap.data() as RepoData;
140+
return json as RepoData;
118141
}
119142

120143
export async function fetchRepoPage(
121144
product: string,
122145
id: string,
123146
pageKey: string
124147
): Promise<RepoPage> {
125-
const ref = reposRef(product).doc(id).collection("pages").doc(pageKey);
148+
const pagePath = `/products/${product}/repos/${id}/pages/${pageKey}`;
149+
const json = await fetchDoc(pagePath);
150+
151+
return json as RepoPage;
152+
}
153+
154+
export async function queryBlogs(
155+
product: string,
156+
q: FirestoreQuery
157+
): Promise<QueryResult<BlogData>> {
158+
const collectionPath = `/products/${product}/blogs`;
159+
const json = await fetchQuery(collectionPath, q);
160+
161+
return json as QueryResult<BlogData>;
162+
}
163+
164+
export async function queryRepos(
165+
product: string,
166+
q: FirestoreQuery
167+
): Promise<QueryResult<RepoData>> {
168+
const collectionPath = `/products/${product}/repos`;
169+
const json = await fetchQuery(collectionPath, q);
126170

127-
const snap = await ref.get();
128-
return snap.data() as RepoPage;
171+
return json as QueryResult<RepoData>;
129172
}

app/src/plugins/firebase.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import firebase from "firebase/app";
2-
import "firebase/firestore";
3-
4-
let _firestore: firebase.firestore.Firestore | null = null;
52

3+
// TODO: Do we need this at all anymore if we're not using Firestore on the frontend?
64
export function app() {
75
if (firebase.apps.length === 0) {
86
if (process.env.VUE_APP_FIREBASE_PROJECT === "ugc-site-prod") {
@@ -30,14 +28,3 @@ export function app() {
3028

3129
return firebase.app();
3230
}
33-
34-
export function firestore() {
35-
if (_firestore === null) {
36-
_firestore = app().firestore();
37-
if (process.env.NODE_ENV !== "production") {
38-
_firestore.useEmulator("localhost", 8001);
39-
}
40-
}
41-
42-
return _firestore;
43-
}

app/src/views/Home.vue

+21-8
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,10 @@ import ProductLogo from "@/components/ProductLogo.vue";
117117
import UIModule from "@/store/ui";
118118
119119
import { ALL_PRODUCTS, ProductConfig } from "@/model/product";
120-
import { blogsRef, getDocs, reposRef } from "@/plugins/data";
120+
import { queryRepos, queryBlogs } from "@/plugins/data";
121121
122122
import { BlogData, RepoData } from "../../../shared/types";
123+
import { FirestoreQuery } from "../../../shared/types/FirestoreQuery";
123124
124125
@Component({
125126
components: {
@@ -135,6 +136,16 @@ export default class Home extends Vue {
135136
public recentBlogs: Record<string, BlogData[]> = {};
136137
public recentRepos: Record<string, RepoData[]> = {};
137138
139+
private RECENTLY_ADDED_QUERY: FirestoreQuery = {
140+
orderBy: [
141+
{
142+
fieldPath: "stats.dateAdded",
143+
direction: "desc",
144+
},
145+
],
146+
limit: 2,
147+
};
148+
138149
mounted() {
139150
const promises: Promise<unknown>[] = [];
140151
@@ -150,15 +161,17 @@ export default class Home extends Vue {
150161
}
151162
152163
public async fetchRecentRepos(product: string) {
153-
const q = reposRef(product).orderBy("stats.dateAdded", "desc").limit(2);
154-
const { data } = await getDocs(q);
155-
Vue.set(this.recentRepos, product, data);
164+
const res = await queryRepos(product, this.RECENTLY_ADDED_QUERY);
165+
const docs = res.docs.map((d) => d.data);
166+
167+
Vue.set(this.recentRepos, product, docs);
156168
}
157169
158170
public async fetchRecentBlogs(product: string) {
159-
const q = blogsRef(product).orderBy("stats.dateAdded", "desc").limit(2);
160-
const { data } = await getDocs(q);
161-
Vue.set(this.recentBlogs, product, data);
171+
const res = await queryBlogs(product, this.RECENTLY_ADDED_QUERY);
172+
const docs = res.docs.map((d) => d.data);
173+
174+
Vue.set(this.recentBlogs, product, docs);
162175
}
163176
164177
public scrollToProducts() {
@@ -181,7 +194,7 @@ export default class Home extends Vue {
181194
(arr) => arr.length > 0
182195
);
183196
184-
return hasRepos || hasBlogs;
197+
return hasRepos && hasBlogs;
185198
}
186199
187200
get products() {

0 commit comments

Comments
 (0)