Skip to content

Commit

Permalink
fix: test rest api
Browse files Browse the repository at this point in the history
  • Loading branch information
th0rgall committed Mar 14, 2024
1 parent 13f8f49 commit f07f574
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 48 deletions.
26 changes: 13 additions & 13 deletions src/lib/api/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getApps, initializeApp, type FirebaseApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check';
import { initializeAppCheck, ReCaptchaV3Provider, type AppCheck } from 'firebase/app-check';
import { connectAuthEmulator, getAuth, type Auth } from 'firebase/auth';
import { type Firestore, getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { connectStorageEmulator, getStorage, type FirebaseStorage } from 'firebase/storage';
Expand All @@ -25,22 +25,19 @@ const FIREBASE_CONFIG = {
};

const messageFor = (str: string) => `Trying to use an uninitialized ${str}.`;
type FirestoreWarning = {
app: string;
firestore: string;
auth: string;
storage: string;
functions: string;
messaging: string;
};
export const FIREBASE_WARNING: FirestoreWarning = [

const firestoreWarningKeys = [
'app',
'firestore',
'auth',
'storage',
'functions',
'messaging'
].reduce(
'messaging',
'appCheck'
] as const;
type FirestoreWarning = { [key in (typeof firestoreWarningKeys)[number]]: string };

export const FIREBASE_WARNING: FirestoreWarning = firestoreWarningKeys.reduce(
(warningsObj, service) => ({ ...warningsObj, [service]: messageFor(service) }),
{}
) as FirestoreWarning;
Expand Down Expand Up @@ -78,6 +75,9 @@ export const db: () => Firestore = guardNull<Firestore>(() => dbRef, 'firestore'
let authRef: Auth | null = null;
export const auth: () => Auth = guardNull<Auth>(() => authRef, 'auth');

let appCheckRef: AppCheck | null = null;
export const appCheck: () => AppCheck = guardNull<AppCheck>(() => appCheckRef, 'appCheck');

let usCentral1FunctionsRef: Functions | null = null;
export const functions: () => Functions = guardNull<Functions>(
() => usCentral1FunctionsRef,
Expand Down Expand Up @@ -130,7 +130,7 @@ export async function initialize(): Promise<void> {
}

if (typeof import.meta.env.VITE_FIREBASE_APP_CHECK_PUBLIC_KEY !== 'undefined') {
initializeAppCheck(appRef, {
appCheckRef = initializeAppCheck(appRef, {
provider: new ReCaptchaV3Provider(import.meta.env.VITE_FIREBASE_APP_CHECK_PUBLIC_KEY),
isTokenAutoRefreshEnabled: true
});
Expand Down
199 changes: 164 additions & 35 deletions src/lib/api/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import {
} from 'firebase/firestore';
import { getUser } from '$lib/stores/auth';
import { isUploading, uploadProgress, allGardens, isFetchingGardens } from '$lib/stores/garden';
import { db, storage } from './firebase';
import { appCheck, db, storage } from './firebase';
import { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage';
import type { Garden, GardenFacilities } from '$lib/types/Garden';
import { get } from 'svelte/store';
import { getToken } from 'firebase/app-check';

/**
* Get a single garden, if it exists and is listed. Returns `null` otherwise.
Expand All @@ -41,49 +42,177 @@ export const getGarden = async (id: string) => {
}
};

type DoubleValue = {
doubleValue: number;
};

type IntegerValue = {
integerValue: number;
};

type StringValue = {
stringValue: string;
};

type BooleanValue = {
booleanValue: boolean;
};

type MapValue = {
mapValue: {
fields: {
[key: string]: StringValue | BooleanValue | DoubleValue | IntegerValue;
};
};
};

type RESTGardenDoc = {
document: {
/**
* Path
*/
name: string;
fields: {
name: StringValue;
description: StringValue;
location: MapValue;
listed: BooleanValue;
facilities: MapValue;
photo: StringValue;
owner: StringValue;
createTime: string;
updateTime: string;
};
createTime: string;
updateTime: string;
};
/**
* ISO string
*/
readTime: string;
};

function mapRestToGarden(doc: RESTGardenDoc): Garden {
const { name, fields } = doc.document;
const { description, location, listed, facilities, photo } = fields;

return {
id: name.split('/').pop() as string,
description: description?.stringValue,
location: {
latitude: location.mapValue.fields.latitude?.doubleValue,
longitude: location.mapValue.fields.longitude?.doubleValue
},
listed: listed.booleanValue,
facilities: {
// Map facilities fields to boolean values
// Assuming all facilities are stored as boolean values or integer values
...Object.fromEntries(
Object.entries(facilities.mapValue.fields).map(([key, value]) => [
key,
typeof value.booleanValue !== 'undefined' ? value.booleanValue : +value.integerValue
])
)
},
photo: photo?.stringValue
};
}

export const getAllListedGardens = async () => {
const CHUNK_SIZE = 5000;
// To prevent endless loops in case of unexpected problems or bugs
// Note: this leads to the loop breaking once this number of gardens is reached!
const LOOP_LIMIT_ITEMS = 100000;

console.log('starting to fetch all gardens...');

isFetchingGardens.set(true);
let startAfterDoc = null;
let iteration = 1;
do {
iteration++;
// Query the chunk of gardens
const q = query.apply(null, [
collection(db(), CAMPSITES),
where('listed', '==', true),
limit(CHUNK_SIZE),
...(startAfterDoc ? [startAfter(startAfterDoc)] : [])
]);
const querySnapshot = await getDocs(q);

if (querySnapshot.size === CHUNK_SIZE) {
// If a full chunk was fetched, there might be more gardens to fetch
startAfterDoc = querySnapshot.docs[querySnapshot.docs.length - 1];
} else {
// If the chunk was not full, there are no more gardens to fetch
startAfterDoc = null;

let appCheckTokenResponse;
try {
appCheckTokenResponse = await getToken(appCheck(), /* forceRefresh= */ false);
} catch (err) {
// Handle any errors if the token was not retrieved.
return;
}

// Include the App Check token with requests to your server.
const apiResponse = (await fetch(
`https://firestore.googleapis.com/v1/projects/${
import.meta.env.VITE_FIREBASE_PROJECT_ID
}/databases/(default)/documents:runQuery`,
{
headers: {
'X-Firebase-AppCheck': appCheckTokenResponse.token
},
method: 'POST',
body: JSON.stringify({
structuredQuery: {
from: [
{
collectionId: 'campsites',
allDescendants: false
}
],
where: {
fieldFilter: {
field: {
fieldPath: 'listed'
},
op: 'EQUAL',
value: {
booleanValue: true
}
}
}
// limit: 100
}
})
}
).then((r) => r.json())) as { document: RESTGardenDoc }[];

const gardens = apiResponse.map(mapRestToGarden);
// NOTE: not doing update anymore
allGardens.set(gardens);

// let startAfterDoc = null;
// let iteration = 1;
// do {
// iteration++;
// // Query the chunk of gardens
// const q = query.apply(null, [
// collection(db(), CAMPSITES),
// where('listed', '==', true),
// limit(CHUNK_SIZE),
// ...(startAfterDoc ? [startAfter(startAfterDoc)] : [])
// ]);
// const querySnapshot = await getDocs(q);

// if (querySnapshot.size === CHUNK_SIZE) {
// // If a full chunk was fetched, there might be more gardens to fetch
// startAfterDoc = querySnapshot.docs[querySnapshot.docs.length - 1];
// } else {
// // If the chunk was not full, there are no more gardens to fetch
// startAfterDoc = null;
// }

// // Merge the map with the existing gardens
// allGardens.update((existingGardens) => {
// querySnapshot.forEach((doc) => {
// const data = doc.data();
// existingGardens.push({
// id: doc.id,
// ...data
// });
// });
// return existingGardens;
// });

// // Wait 1000ms seconds before fetching the next chunk
// // await new Promise<void>((resolve) => setTimeout(() => resolve()));
// } while (startAfterDoc != null && iteration < LOOP_LIMIT_ITEMS / CHUNK_SIZE);

// Merge the map with the existing gardens
allGardens.update((existingGardens) => {
querySnapshot.forEach((doc) => {
const data = doc.data();
existingGardens.push({
id: doc.id,
...data
});
});
return existingGardens;
});

// Wait 1000ms seconds before fetching the next chunk
// await new Promise<void>((resolve) => setTimeout(() => resolve()));
} while (startAfterDoc != null && iteration < LOOP_LIMIT_ITEMS / CHUNK_SIZE);
// console.log('Fetched all gardens!');

isFetchingGardens.set(false);
return get(allGardens);
Expand Down
2 changes: 2 additions & 0 deletions src/lib/stores/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const isRegistering = writable(false);
export const isUserLoading = writable(false);
export const user: Writable<User | null> = writable(null);

export const appCheckToken = writable<string | null>(null);

export const resolveOnUserLoaded = async () => {
// Guard is necesary, because if !isUserLoading, the reference
// to unsubFromLoading won't be created
Expand Down

0 comments on commit f07f574

Please sign in to comment.