Firebase/NextJS: Can't access subcollections for dynamic routing - firebase

I'm building a story editor where you can write stories and separate them into different chapters and I'm having trouble accessing my subcollection for NextJS's "getStaticPaths" because every document is a UUID.
I have data stored in a Firebase Firestore like this:
stories
├── UUID1 (Story 1)
│ └──chapters
│ └── UUID (Chapter 1)
│ └── UUID (Chapter 2)
│
│
├── UUID2 (Story 1)
│ └──chapters
│ └── UUID (Chapter 1)
│ └── UUID (Chapter 2)
│
Problem
I can't access the chapters subcollection because I can't fetch the UUIDs.
Normally, to access the chapters, I create a useEffect hook to store the UUID into a useState hook so I can send a query like the code below:
const chaptersQ = query(
collection(db, "stories", story.data().id, "chapters")
);
Unfortunately, in trying to create dynamic routing and "getStaticPaths" exist before/outside the main function, so I can't use react hooks.
Here's my current code:
export const getStaticPaths = async () => {
const storyQ = collection(db, "stories");
const queryStory = await getDocs(storyQ);
queryStory.forEach(async (story) => {
const chaptersQ = query(
collection(db, "stories", story.data().id, "chapters")
);
const queryChapters = getDocs(chaptersQ);
return {
paths: (await queryChapters).docs?.map((doc) => ({
params: {
id: `${doc.data().storyID}`,
chapter: `${doc.data().chapter}`,
},
})),
};
});
};
This returns an invalid error, as the paths somehow receive an undefined.
I've spent hours working on this. The closest I've gotten is by declaring a variable with "let" in the root layer of the file and then pushing chapter doc.data into it.
Then using that in the return statement to set the paths and parameters. However, this also returned and undefined error.
Thank you in advance!

The forEach method does not return a value, so the paths property will not be set correctly, meaning it will be undefined.
In my opinion there are 2 ways you can get the desired result:
Instead of forEach, you can use the map function to create an array of paths and then return it.
export const getStaticPaths = async () => {
const storyQ = collection(db, "stories");
const queryStory = await getDocs(storyQ);
const paths = await Promise.all(queryStory.map(async (story) => {
const chaptersQ = query(
collection(db, "stories", story.data().id, "chapters")
);
const queryChapters = await getDocs(chaptersQ);
return queryChapters.docs.map((doc) => ({
params: {
id: `${doc.data().storyID}`,
chapter: `${doc.data().chapter}`,
},
}));
}));
return {
paths: paths.flat(),
};
};
This should correctly create the paths array, but you may need to adjust it to your specific use case.
Using async/await and creating an empty array and then push the paths in it.
export const getStaticPaths = async () => {
const storyQ = collection(db, "stories");
const queryStory = await getDocs(storyQ);
let paths = []
for (const story of queryStory) {
const chaptersQ = query(
collection(db, "stories", story.data().id, "chapters")
);
const queryChapters = await getDocs(chaptersQ);
for(const chapter of queryChapters.docs) {
paths.push({
params: {
id: `${chapter.data().storyID}`,
chapter: `${chapter.data().chapter}`,
},
});
}
}
return {
paths,
};
};
If I were given the choice of picking which approach to be used I would choose the 1st one. As Promise.all() gives clean code.

Related

Can i create a Nextjs dynamic route [id]-[first_name]-[last_name]?

I am using nextjs to build a directory. I effectively want to click on 'more info' and an info page to load under the URL of /info/[id]-[first_name]-[last_name].
I am pulling data from an api by id, which will then get the first_name and last_name data.
I have a file inside an info folder named [id]-[first_name]-[last_name] :
export default function Info({ info }) {
return (
<div>
<h1>First Name</h1>
<p> Last Name </p>
</div>
);
}
export const getStaticPaths = async () => {
const res = await fetch('http://xxx:1337/api/info');
const data = await res.json();
// map data to an array of path objects with params (id)
const paths = [data].map(info => {
return {
params: [{
id: `${info.id}-`,
first_name: `${info.first_name}-`,
last_name: `${info.last_name}`
}]
}
})
return {
paths,
fallback: false
}
}
export const getStaticProps = async (context) => {
const id = context.params.id;
const res = await fetch('http://xxxx:1337/api/info/' + id);
const data = await res.json();
return {
props: { info: data }
}
With this I just get the error:
Error: A required parameter (id]-[first_name]-[last_name) was not provided as a string in getStaticPaths for /info/[id]-[first_name]-[last_name]
I guess that error is pretty self-explanatory, but I am blocked at this point. I have seen that i may be able to use a slug, but that means re-working a lot of the api.
Any direction with this is apprecated. Thanks!
in this way you can catch all attributes /info/[id]/[first_name]/[last_name]
by making the file /info/[...slug]
export const getStaticProps = async ({ query }) => {
const [id ,firstname ,lastname] = query.slug }
or keep it /info/[slug] and get it as string after that you can split it

How to remove data from the item page when moving to another page using nuxt 3?

I just started learning Nuxt3. In my project I get list of movies from an API:
<script setup>
const config = useAppConfig();
let page = ref(1);
let year = 2022;
let url = computed(() => `https://api.themoviedb.org/3/discover/movieapi_key=${config.apiKey}&sort_by=popularity.desc&page=${page.value}&year=${year}`);
const { data: list } = await useAsyncData("list", () => $fetch(url.value));
const next = () => {
page.value++;
refreshNuxtData("list");
};
const prev = () => {
if (page.value > 1) {
page.value--;
refreshNuxtData("list");
}
};
</script>
Then I have a page for each movie where I get information about it:
<script setup>
const config = useAppConfig();
const route = useRoute();
const movieId = route.params.id;
const url = `https://api.themoviedb.org/3/movie/${movieId}api_key=${config.apiKey}`;
const { data: movie } = await useAsyncData("movie", () => $fetch(url));
refreshNuxtData("movie");
</script>
My problem is that when I open a new movie page, I see information about the old one, but after a second it changes. How can I fix it?
And I have doubts if I'm using refreshNuxtData() correctly. If not, can you show me the correct example of working with API in Nuxt3?
OP fixed the issue by using
const { data: movie } = await useFetch(url, { key: movieId })
movieId being dynamic, it will dedupe all the calls as explained here for the key: https://v3.nuxtjs.org/api/composables/use-async-data/#params
key: a unique key to ensure that data fetching can be properly de-duplicated across requests. If you do not provide a key, then a key that is unique to the file name and line number of the instance of useAsyncData will be generated for you

Vue3 - OnMount doesn´t load array

In Vue 3 i need to fill some array with result of store. I import store like this
Imports
import { onMounted, ref, watch } from "vue";
import { useTableStore } from "../stores/table";
Then i declare values and try to fill it
const search = ref(null);
const searchInput = ref("");
const edition = ref([]);
const compilation = ref([]);
const debug = ref([]);
const navigation = ref([]);
const refactoring = ref([]);
const store = useTableStore();
onMounted(() => {
store.fetchTable();
edition.value = store.getEdition;
compilation.value = store.getCompilation;
debug.value = store.getDebug;
navigation.value = store.getNavigation;
refactoring.value = store.getRefactoring;
});
Values doesn´t fill it. Is strange, if use watcher like this
edition.value = store.getEdition.filter((edition: String) => {
for (let key in edition) {
if (
edition[key].toLowerCase().includes(searchInput.value.toLowerCase())
) {
return true;
}
}
});
Array get values.
So, the problem is: How can i get store values when view loads?
Maybe the problem is the store returns Proxy object...
UPDATE 1
I created a gist with full code
https://gist.github.com/ElHombreSinNombre/4796da5bcdcf6bf4f36f009132dd9f48
UPDATE 2
Pinia loads array data, but 'setup' can´t get it
UPDATE 3: SOLUTION
Finally i resolved the problems and upload to my Github. I used computed to get data updated. Maybe other solution was better.
https://github.com/ElHombreSinNombre/vue-shortcuts
Your onMounted lambda needs to be async, and you need to wait the fetchTable function. Edit: Try using reactive instead of ref for your arrays. Rule of thumb is ref for primitive values and reactive for objects and arrays.
const search = ref(null);
const searchInput = ref("");
const edition = reactive([]);
const compilation = reactive([]);
const debug = reactive([]);
const navigation = reactive([]);
const refactoring = reactive([]);
const store = useTableStore();
onMounted(async () => {
await store.fetchTable();
edition.push(...store.getEdition);
compilation.push(...store.getCompilation);
debug.push(...store.getDebug);
navigation.push(...store.getNavigation);
refactoring.push(...store.getRefactoring);
});
If what you need is the component to not be rendered until data is ready, you'll need a flag in your data that works along with a v-if to render the component when everything is ready, something like this:
// in your template
<div v-if="dataReady">
// your html code
</div>
// inside your script
const dataReady = ref(false)
onMounted(async () => {
await store.fetchTable();
dataReady.value = true;
});

Cloud Functions, Firestore trigger, recursive delete not working

Currently, I`m trying to delete all the nested hirachy datas when some documents deleted using Firestore Trigger
and my code and error code are below
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
}
);
exports.firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (change: any, context: any) => {
const path = change.path;
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and the Error code is below
FirebaseError: Must specify a path.
at Object.reject (/workspace/node_modules/firebase-tools/lib/utils.js:122:27)
why it shows error code? I do not want "callable functions" because of security
Don't ever use any when writing in TypeScript as you essentially "switch off" TypeScript and lose valuable type information such as why your code isn't working.
If you use the following lines, the type of change (which should be snapshot) and context are automatically set as QueryDocumentSnapshot and EventContext respectively as defined by the onDelete method.
exports.firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (snapshot, context) => {
});
Note: A QueryDocumentSnapshot is like a regular DocumentSnapshot, but it is guaranteed to exist (snap.exists always returns true and snap.data() will never return undefined).
Now that the type is restored to snapshot, you'll see that you can't read a property called path from it as it's not set. In your current code, path will just be set to undefined, which leads to the error saying you need to specify a path.
const path = snapshot.path; // ✖ Property 'path' does not exist on type 'QueryDocumentSnapshot'. ts(2339)
This is because path is a property of DocumentReference. To get the snapshot's underlying DocumentReference, you access it's ref property.
const path = snapshot.ref.path; // ✔
Next, in Node 10+ Cloud Functions, the list of available environment variables was changed and process.env.GCLOUD_PROJECT was removed. To get the current project ID, you can use either
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
or
const defaultApp = admin.intializeApp();
const PROJECT_ID = defaultApp.options.projectId;
This then makes your (modernized) code:
import * as admin from 'firebase-admin';
import * as firebase_tools from 'firebase-tools';
import * as functions from 'firebase-functions';
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
admin.initializeApp(); // config is automatically filled if left empty
// although it isn't needed here
export const firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (snapshot, context) => {
const path = snapshot.ref.path;
await firebase_tools.firestore
.delete(path, {
project: PROJECT_ID,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});

Error when unit testing cloud function: no document found to update

I am following the firebase documentation for unit testing. I created a simple cloud function that is triggered when a firestore document is created. It turns the value in that document to uppercase. I tried the function by creating a document and it worked as expected.
I use jest for the test. When I run the test I get the following error:
NOT_FOUND: No document to update: projects/myproject/databases/(default)/documents/test/testId
10 | return admin.firestore().doc(`test/` + snap.id)
> 11 | .update({ input: input });
| ^
12 | });
I added a console.log to my function to see what data was passed by the unit-test. It passes the right data (meaning the right document id and the right value for "input".
I'm not sure what I'm missing here. It's a simple function and a simple test. I don't understand how the function could receive the right data but not found the document, or how the document is not created in firestore when the test is run.
The function:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
export const makeUpperCase = functions.firestore
.document('test/{id}')
.onCreate((snap, context) => {
const data = snap.data();
const input = data.input.toUpperCase();
console.log('new data: ' + input + ', id: ' + snap.id);
return admin.firestore().doc('test/' + snap.id)
.update({ input: input });
});
index.ts:
import * as admin from 'firebase-admin';
admin.initializeApp();
export { makeUpperCase } from './makeUpperCase';
the test (basicTest.test.ts):
import * as functions from 'firebase-functions-test';
import * as admin from 'firebase-admin';
import 'jest';
const testEnv = functions({
databaseURL: "https://mydb.firebaseio.com",
projectId: "myproject",
}, './service-account.json');
testEnv.mockConfig({});
import { makeUpperCase } from '../src/index';
describe('basicTest', () => {
let wrapped: any;
beforeAll(() => {
wrapped = testEnv.wrap(makeUpperCase);
});
test('converts input to upper case', async () => {
const path = 'test/testId';
const data = { input: 'a simple test' };
const snap = await testEnv.firestore.makeDocumentSnapshot(data, path);
await wrapped(snap);
const after = await admin.firestore().doc(path).get();
expect(after.data()!.input).toBe('A SIMPLE TEST');
});
});
The problem comes from your use of the import syntax. Unlike require, the import instructions are performed asynchronously before the script that is importing them is executed.
import * as module1 from 'module1';
console.log(module1);
import * as module2 from 'module2';
console.log(module1);
is effectively (not exactly) the same as
const module1 = require('module1');
const module2 = require('module2');
console.log(module1);
console.log(module2);
Because of this quirk, in your current code, admin.firestore() is incorrectly pointing at your live Firestore database and test.firestore is pointing at your test Firestore database. This is why the document doesn't exist, because it exists on your test database, but not your live one.
This is why the documentation you linked uses require statements, and not the import syntax.
// this is fine, as long as you don't call admin.initializeApp() before overriding the config and service account as part of test's initializer below
import * as admin from 'firebase-admin';
import 'jest';
// Override the configuration for tests
const test = require("firebase-functions-test")({
databaseURL: "https://mydb.firebaseio.com",
projectId: "myproject",
}, './service-account.json');
test.mockConfig({});
// AFTER mocking the configuration, load the original code
const { makeUpperCase } = require('../src/index');
describe('basicTest', () => {
let wrapped: any;
beforeAll(() => {
wrapped = test.wrap(makeUpperCase);
});
test('converts input to upper case', async () => {
const path = 'test/testId';
const data = { input: 'a simple test' };
const snap = await test.firestore.makeDocumentSnapshot(data, path);
await wrapped(snap);
const after = await admin.firestore().doc(path).get();
expect(after.data()!.input).toBe('A SIMPLE TEST');
});
});

Resources