Node.js promise fails intermittently, even when handled - javascript

I'm learning to use MongoDB by creating a simple blog app. However, a portion of my code that saves a given post seems to give problems with promises occasionally, but not always, and whether the code succeeds simply seems to be luck.
Each post in my database is stored with the following schema:
{
title: String,
author: String,
body: String,
slug: String,
baseSlug: String,
published: { type: Boolean, default: false }
}
The slug defines the link used to access the blog post, and is automatically generated based upon the title of the blog post. However, if article titles are duplicates, the slug will have a number added to the end to differentiate it from similar articles, while the baseSlug will remain the same. For example:
I create the post "My first post", and it is assigned the baseSlug of "my-first-post". Because no other posts have the same baseSlug, the slug is also set to be "my-first-post".
I create another post called "My first post", and it is assigned the baseSlug of "my-first-post". However, because another post has the same baseSlug, it is assigned the slug "my-first-post-1".
To create this behavior, I wrote the following addpost route in Express:
app.post("/addpost", (req, res) => {
let postInfo = req.body;
for (key of Object.keys(postInfo)) {
if (postInfo[key] == "true") postInfo[key] = true;
}
let slug = postInfo.title
.toLowerCase()
.split(" ")
.filter(hasNumber) // return /\d/.test(str);
.slice(0, 5)
.join("-");
postInfo.slug = slug;
var postData;
Post.find({ baseSlug: postInfo.slug }, (error, documents) => {
if (documents.length > 0) {
let largestSlugSuffix = 0;
for (let document of documents) {
var fullSlug = document.slug.split("-");
var suffix = fullSlug[fullSlug.length - 1];
if (!isNaN(suffix)) {
if (parseInt(suffix) > largestSlugSuffix) {
largestSlugSuffix = suffix;
}
}
}
largestSlugSuffix++;
postInfo.baseSlug = postInfo.slug;
postInfo.slug += "-" + largestSlugSuffix;
} else {
postInfo.baseSlug = postInfo.slug;
}
postData = new Post(postInfo);
})
.then(() => {
postData
.save()
.then(result => {
res.redirect("/");
})
.catch(err => {
console.log(err);
res.status(400).send("Unable to save data");
});
})
.catch(err => {
console.log(err);
res.status(400).send("Unable to save data");
});
});
This code seems to work most of the time, but sometimes it fails, and outputs the following:
TypeError: Cannot read property 'save' of undefined
at C:\Users\User\BlogTest\app.js:94:18
at processTicksAndRejections (internal/process/task_queues.js:94:5)
(For reference, line 94 in my file is postData.save())
I suspect it is because the main body of the function takes longer than it should to execute, and the postData variable is not yet defined. However, postData.save() should not be executed until the promise finishes, because of the .then() callback function.
Why is my code behaving like this? Is there any way to fix it?

The issue is that you are mixing promises with callbacks and closures. That's not how this is intended to work.
When you chain promises, whatever you return in the first promise handler will be added as an input to the next one. And if you return a promise, that promise will be resolved first before being sent to the next thenable.
So you need to return promises from your promises, like this:
app.post("/addpost", (req, res) => {
let postInfo = req.body;
for (key of Object.keys(postInfo)) {
if (postInfo[key] == "true") postInfo[key] = true;
}
let slug = postInfo.title
.toLowerCase()
.split(" ")
.filter(hasNumber) // return /\d/.test(str);
.slice(0, 5)
.join("-");
postInfo.slug = slug;
// var postData; <-- Don't do that
Post.find({ baseSlug: postInfo.slug })
.then((documents) => {
if (documents.length > 0) {
let largestSlugSuffix = 0;
for (let document of documents) {
var fullSlug = document.slug.split("-");
var suffix = fullSlug[fullSlug.length - 1];
if (!isNaN(suffix)) {
if (parseInt(suffix) > largestSlugSuffix) {
largestSlugSuffix = suffix;
}
}
}
largestSlugSuffix++;
postInfo.baseSlug = postInfo.slug;
postInfo.slug += "-" + largestSlugSuffix;
} else {
postInfo.baseSlug = postInfo.slug;
}
return new Post(postInfo);
// We could actually have called postData.save() in this method,
// but I wanted to return it to exemplify what I'm talking about
})
// It is important to return the promise generated by postData.save().
// This way it will be resolved first, before invoking the next .then method
.then( (postData) => { return postData.save(); })
// This method will wait postData.save() to complete
.then( () => { res.redirect("/"); })
.catch( (err) => {
console.log(err);
res.status(400).send("Unable to save data");
});
});
It can be greatly simplified with async/await:
app.post("/addpost", async (req, res) => {
try {
let postInfo = req.body;
for (key of Object.keys(postInfo)) {
if (postInfo[key] == "true") postInfo[key] = true;
}
let slug = postInfo.title
.toLowerCase()
.split(" ")
.filter(hasNumber)
.slice(0, 5)
.join("-");
postInfo.slug = slug;
let documents = await Post.find({ baseSlug: postInfo.slug });
if (documents.length > 0) {
let largestSlugSuffix = 0;
for (let document of documents) {
var fullSlug = document.slug.split("-");
var suffix = fullSlug[fullSlug.length - 1];
if (!isNaN(suffix)) {
if (parseInt(suffix) > largestSlugSuffix) {
largestSlugSuffix = suffix;
}
}
}
largestSlugSuffix++;
postInfo.baseSlug = postInfo.slug;
postInfo.slug += "-" + largestSlugSuffix;
} else {
postInfo.baseSlug = postInfo.slug;
}
let postData = new Post(postInfo);
await postData.save();
res.redirect("/");
} catch (err) {
console.log(err);
res.status(400).send("Unable to save data");
};
});

You are mixing callbacks and promises and while it may do something, I'm not sure what it will do exactly. You should pick one or the other and not mix them as much as possible. I would recommend picking promises if you are using a language that supports async/await, otherwise callbacks.
So for example your outter handler could be an async function
app.post("/addpost", async (req, res) => {
//...
})
Your real bug is in handling Post.find you are handling it somewhat with a callback and somewhat with a promise, and probably whats happening is that its random which one will get called first the callback or the promise resolution. Instead of both you should just do this now that you have an async function:
try {
const posts = await Post.find({ baseSlug: postInfo.slug });
// stuff you were doing in the callback
const post = new Post(postInfo)
// Now the promise code
await post.save()
// success!
res.redirect("/");
} catch (err) {
// With an async function you can just catch errors like normal
console.log(err);
res.status(400).send("Unable to save data");
}
If you're not using webpack or typescript and cannot target es7 then and thus cannot use async/await then I would recommend just using callbacks, do not use .then or .catch and that would look more like:
function error(err) {
console.log(err)
res.status(400).send("Unable to save data")
}
Post.find({ baseSlug: postInfo.slug }, (err, documents) => {
if (err) return error(err)
// stuff you're doing in the callback now
const post = new Post(postInfo)
post.save((err) => {
if (err) return error(err)
// success!
res.redirect("/");
})
})

Related

JS async function returning undefined although calling it inside an asynchronous function

I am using nodeJs Express and I have an async function which is used in another function.
here is my code:
/* get user info */
const getUserInfo = async (req) => {
let userID = req.cookies.userId;
if(!userID) return null;
await connection.query('SELECT * FROM users WHERE id = ' + userID,
function (err, rows, fields) {
if (err) throw err
// if user not found
if (rows.length === 0) {
return null;
}
// if user found
else {
return rows[0];
}
});
}
/* display dashboard page */
router.get("/", async function (req, res, next) {
let userInfo = await getUserInfo(req);
console.log(userInfo) // It's result is undefined
if (userInfo) {
res.render('dashboard/profile', {
fullName: userInfo.fname + " " + userInfo.lname,
email: userInfo.email,
phone: userInfo.phone,
});
}
else {
res.render('authentication/register', {
title: 'ثبت نام',
});
}
});
how to resolve this problem? I need userInfo retun me some data.
await is only useful if the value on the right-hand side is a promise.
connection.query is taking a callback function, which implies it isn't returning a promise.
You need to either:
Find out how to make the API you are using return a promise (if that is possible)
Wrap the callback in a promise
Replace the API with one that supports promises natively
You also need getUserInfo to have a return statement of its own.
You have to return some value from your getUserInfo function
If connection query doesn't support promise you should wrap it like this
/* get user info */
const getUserInfo = async(req) => {
let userID = req.cookies.userId;
if (!userID) return null;
return new Promise((resolve, reject) => {
connection.query('SELECT * FROM users WHERE id = ' + userID,
function(err, rows, fields) {
if (err) {
reject(err)
return;
}
// if user not found
if (rows.length === 0) {
resolve(null)
}
// if user found
else {
resolve(rows[0]);
}
});
});
}

Trying to return an ID using Wix Velo and Promises

I'm trying to retrieve the _id field with Wix Velo of my logged in user. The code is working, however the function I'm using is returning [object Promise] instead of the id string that I need.
import wixData from 'wix-data';
export function getID() {
return wixData.query("Picks") // My Collection
.eq("player", "Gary") // A field, and a cell
.find()
.then((results) => {
if (results.items.length > 0) {
let firstItem = results.items[0]._id;
console.log("Return ID: " + firstItem) // This returns the ID I need
$w("#text31").text = firstItem;
// return firstItem.toString();
} else {
console.log("No Items Found")
}
})
.catch((err) => {
let errorMsg = err;
});
}
console.log("Return the ID outside of the function: " + getID()) // This returns "[object Promise]"
I've tried to use await, but it just gives me errors.
Since top-level-await is not supported by all the browsers, I believe Wix didn't open that feature as well. Instead, wrap it with an async IIFE.
(async () => {
console.log("Return the ID outside of the function: " + await getID())
})();
This answer has more details about top-level-await
Just incase this helps someone else, the answer was in nested promises. I had to call the first function getLoggedInUserName() to get the user name, and then pass that to a nested function called getID().
Essentially, this was a scope issue, and also waiting for the promise to be resolved.
getLoggedInUserName().then((results) => {
let userName = results;
getID(userName).then((results) => {
let userID = results;
let pick = $w("#dropdown1").value;
let toUpdate = {
"_id": userID,
"pick": pick,
"player": userName,
}
wixData.update("Picks", toUpdate)
.then((results) => {
$w("#text33").show();
let item = results;
})
.catch((err) => {
let errorMsg = err;
console.log("Error: " + errorMsg)
});
})
})
.catch((err) => {
let errorMsg = err;
});

Axios requests with express, node, ejs

I am working on a site using Express.js, node.js, Axios, and ejs. I am making REST calls to a Oracle SQL REST services using Axios. I am having trouble working with Promises or Async/Await. I could use some guidance, if possible.
I have a repository layer to interface with the Oracle DB. For example:
dataaccess.js
const axios = require('axios');
exports.IsManufacturerCategory = function (categoryId) {
axios.get(`DB ADDRESS ${categoryId}`)
.then(response => {
console.error('GET IsManufacturerCategory categoryId = ' + categoryId);
console.error('Response = ' + JSON.stringify(response.data));
return (response.data);
})
.catch(rej => {
console.error('ERROR IsManufacturerCategory categoryId = ' + categoryId);
console.error('ERR = \n' + rej.data);
return (rej.data);
});
}
Which is called in my middleware. When I call var isManufacturerCat = exports.IsManufacturerCategory(categoryId); it is undefined. I am attempting to use the data retrieved from the Axios call to return a ejs view to my router, which I can provide if needed.
category.js
var isManufacturerCat = exports.IsManufacturerCategory(categoryId);
if (isManufacturerCat) {
var models = dataaccess.GetCategorySubCategories(categoryId);
return ("manufacturers", {
data: {
Canonical: cononical,
Category: category,
IsAManufacturerCategory: iAManufacturerCat,
Models: models
}
});
}
I am open to any advice in my project structure, usage of Promises, Async/Await, etc.
Thank you in advance.
EDIT
After working with some of the answers given, I have made some progress but I am having issues with layers of async calls. I end up getting into a spot where I need to await a call, but I am in a function that I am not able/do not want to do so (i.e. my router).
indexMiddleware.js
exports.getRedirectURL = async function (fullOrigionalpath) {
if (fullOrigionalpath.split('.').length == 1 || fullOrigionalpath.indexOf(".aspx") != -1) {
if (fullOrigionalpath.indexOf(".aspx") != -1) {
//some string stuff to get url
}
else if (fullOrigionalpath.indexOf("/solutions/") != -1) {
if (!fullOrigionalpath.match("/solutions/$")) {
if (fullOrigionalpath.indexOf("/t-") != -1) {
//some stuff
}
else {
var solPart = fullOrigionalpath.split("/solutions/");
solPart = solPart.filter(function (e) { return e });
if (solPart.length > 0) {
var solParts = solPart[solPart.length - 1].split("/");
solParts = solParts.filter(function (e) { return e });
if (solParts.length == 1) {
waitForRespose = true;
const isASolutionCategory = await dataaccess.isASolutionCategory(solParts[0]); // returns void
if (isASolutionCategory != undefined && isASolutionCategory.length > 0 && isASolutionCategory[0].Count == 1) {
// set redirecturl
}
}
else {
redirecturl = "/solutions/solutiontemplate";
}
}
}
}
}
else if (URL STUFF) {
// finally if none of the above fit into url condition then verify current url with Category URL or product url into database and if that matches then redirect to proper internal URL
if (fullOrigionalpath.lastIndexOf('/') == (fullOrigionalpath.length - 1)) {
fullOrigionalpath = fullOrigionalpath.substring(0, fullOrigionalpath.lastIndexOf('/'));
}
waitForRespose = true;
const originalURL = await exports.getOriginalUrl(fullOrigionalpath); //returns string
redirecturl = originalURL;
return redirecturl;
}
}
if (!waitForRespose) {
return redirecturl;
}
}
exports.getOriginalUrl = async function (friendlyUrl) {
var originalUrl = '';
var urlParts = friendlyUrl.split('/');
urlParts = urlParts.filter(function (e) { return e });
if (urlParts.length > 0) {
var skuID = urlParts[urlParts.length - 1];
const parts = await dataaccess.getFriendlyUrlParts(skuID); //returns void
console.log("Inside GetOriginalUrl (index.js middleware) FriendlyUrlParts: " + parts);//undefined
if (parts != undefined && parts != null && parts.length > 0) {
//some stuff
}
else {
// verify whether it's category URL then return the category local URL
console.log('Getting CategoryLocalUrl');
const categoryLocalUrl = await dataaccess.getCategoryLocalUrl(friendlyUrl); // returns void
console.log('CategoryLocalUrl Gotten ' + JSON.stringify(categoryLocalUrl)); //undefined
if (categoryLocalUrl != undefined && categoryLocalUrl.length > 0) {
//set originalUrl
return originalUrl;
}
}
}
else { return ''; }
}
index.js router
router.use(function (req, res, next) {
//bunch of stuff
index.getRedirectURL(url)
.then(res => {
req.url = res;
})
.catch(error => {
console.error(error);
})
.finally(final => {
next();
});
}
I am getting undefined in my console.logs after the awaits. I'm not really sure what I'm doing I guess.
Let's start with dataaccess.js. Basically, you're exporting a function that's doing async work, but the function isn't async. Most people want to be able to utilize async/await, so rather than have IsManufacturerCategory accept a callback function, it would better to have the function return a promise. The easiest way to do that is to make the function an async function. Async functions return promises which are resolved/rejected more easily than by returning an explicit promise. Here's how that could be rewritten:
const axios = require('axios');
exports.IsManufacturerCategory = async function (categoryId) {
try {
const response = await axios.get(`DB ADDRESS ${categoryId}`);
console.log('GET IsManufacturerCategory categoryId = ' + categoryId);
console.log('Response = ' + JSON.stringify(response.data));
} catch (err) {
console.error('ERROR IsManufacturerCategory categoryId = ' + categoryId);
console.error('ERR = \n' + rej.data);
throw err;
}
}
Note that I'm using console.error only for errors. Because you wanted to log some error related details, I used a try/catch statement. If you didn't care about doing that, the function could be simplified to this:
const axios = require('axios');
exports.IsManufacturerCategory = async function (categoryId) {
const response = await axios.get(`DB ADDRESS ${categoryId}`);
console.log('GET IsManufacturerCategory categoryId = ' + categoryId);
console.log('Response = ' + JSON.stringify(response.data));
}
This is because async functions automatically swallow errors and treat them as rejections - so the promise returned by IsManufacturerCategory would be rejected. Similarly, when an async function returns a value, the promise is resolved with the value returned.
Moving on to category.js... This looks strange because you're accessing IsManufacturerCategory from the exports of that module when I think you mean to access it from the import of the dataaccess module, right?
Inside this function, you should put any async work in an async function so that you can use await with function that return promises. Here's how it could be rewritten:
const dataaccess = require('dataccess.js');
async function validateManufacturerCat(categoryId) {
const isManufacturerCat = await dataaccess.IsManufacturerCategory(categoryId);
if (isManufacturerCat) {
const models = await dataaccess.GetCategorySubCategories(categoryId);
return ({
manufacturers: {
data: {
Canonical: cononical,
Category: category,
IsAManufacturerCategory: iAManufacturerCat,
Models: models
}
}
});
}
}
validateManufacturerCat(categoryId)
.then(res => {
console.log(res);
})
.catch(err => {
console.error(err);
});
A couple notes:
I changed the return value in the if statement to be a single value. You should try to always return a single value when working with promises and async/await (since you can only resolve/return one value).
I see lots of functions starting with capital letters. There's a convention in JavaScript where functions with capital letters are constructor functions (meant to be invoked with the new keyword).
Here is a working example. Note that you need a callback function to get the data when it is available from the promise
//client.js
const axios = require("axios")
exports.IsManufacturerCategory = function (url, callback) {
axios.get(url)
.then(response=>
{
callback(response.data);
})
.catch(error=>{
callback( error);
})
};
//app.js
const client = require('./client');
function process(data) {
console.log(data)
}
client.IsManufacturerCategory("http://localhost:3000/persons",process);
documentation

Use async await inside mapping function

In my Node Express App, I want to get all comments for a lesson and modify each comment by adding a fullname field to each comment. For getting full name of a user, I have defined findFullNameByUserId function in UserController.js. I use findAllCommentsByLessonId() in CourseController.js as follows. However, when I'm using it, I always get an empty array. How can I use findFullNameByUserId in findAllCommentsByLessonId() so that fullname field can be added to each comment object?
CourseController.js
findAllCommentsByLessonId: async (courseId,lessonId,callback) => {
Course.find({_id: courseId}, function(err, course) {
if (err) {
callback(err, null);
} else {
const lesson = course[0].lessons.filter(l => l._id !== lessonId)
const comments = lesson[0].comments.map( async c => {
try{
const name = await UserController.findFullNameById(c.user)
return (
{
userId: c.user,
name: name,
content: c.content
}
)
}
catch(err){
console.log(err)
}
})
// console.log(comments) --> This always prints [{}]
callback(null, comments)
}
});
}
UserController.js
module.exports = {
findFullNameById: async (userId) => {
await User.find({_id: userId}, function(err, user) {
if (err) {
console.log(err)
} else {
return user[0].name+" "+( user[0].lname ? user[0].lname : "")
}
});
}
}
in CourseController.js either you can use async-await or you can use callback
async-await way :
findAllCommentsByLessonId: async (courseId,lessonId) => {
let course = await Course.findOne({_id: courseId});
if (course){
let lesson = course.lessons.find(l => l._id == lessonId);
let comments = [];
for(let comment of lesson.comments){
let name = await UserController.findFullNameById(comment.user);
comments.push({userId: comment.user,name: name,content: comment.content});
}
return comments;
}else{
return [];
}
}
callback way :
findAllCommentsByLessonId: (courseId,lessonId,callback) => {
Course.findOne({_id: courseId},function(err, course) {
if (err) {
callback(err, null);
} else {
let lesson = course.lessons.find(l => l._id == lessonId);
let comments = lesson.comments;
comments.map((comment)=>{
UserController.findFullNameById(comment.user).then(name=>{
return {userId: comment.user,name: name,content: comment.content};
});
});
callback(null, comments);
}
});
}
Start by dropping callbacks and actually using promises for await. You haven't specified what find is, but chances are it's a modern library supporting promises. So write
async function findAllCommentsByLessonId(courseId, lessonId) {
const [course] = Course.find({_id: courseId};
const lesson = course.lessons.find(l => l._id !== lessonId);
const comments = lesson.comments.map(async c => {
const name = await UserController.findFullNameById(c.user)
return {
userId: c.user,
name,
content: c.content
};
});
}
module.exports.findFullNameById = async (userId) => {
const [user] = await User.find({_id: userId});
return user.name + " " + (user.lname ? user.lname : "");
};
Then you'll notice that comments is not an array of users, but rather an array of promises for users, so wrap it in a await Promise.all(…) call.
As #SuleymanSah commented, I tried to use .populate and it worked well. I think this is the correct approach as for the exact reasons he's pointed out. The following is how I did it:
Lesson.findOne({ _id: lessonId }).
populate('comments.user').
exec(function(err, lesson) {
if (err) {
console.log(err);
return callback(err, null);
}
if (!lesson) {
console.log("No record found");
return callback(err, null);
}
return callback(null, lesson.comments);
});

How to achieve recursive Promise calls in Node.js

I am calling an API where I can only fetch 1000 records per request,
I was able to achieve this using recursion.
I am now trying to achieve the same using promises, I am fairly new to Node.js and JavaScript too.
I tried adding the recursion code in an if else block but failed
var requestP = require('request-promise');
const option = {
url: 'rest/api/2/search',
json: true,
qs: {
//jql: "project in (FLAGPS)",
}
}
const callback = (body) => {
// some code
.
.
.//saving records to file
.
//some code
if (totlExtractedRecords < total) {
requestP(option, callback).auth('api-reader', token, true)
.then(callback)
.catch((err) => {
console.log('Error Observed ' + err)
})
}
}
requestP(option).auth('api-reader', token, true)
.then(callback)
.catch((err) => {
console.log('Error Observed ' + err)
})
I want to execute the method using promise and in a synchronous way,
i.e. I want to wait until the records are all exported to a file and continue with my code
I think its better to create your own promise and simply resolve it when your done with your recursion. Here's a simply example just for you to understand the approach
async function myRecursiveLogic(resolveMethod, ctr = 0) {
// This is where you do the logic
await new Promise((res) => setTimeout(res, 1000)); // wait - just for example
ctr++;
console.log('counter:', ctr);
if (ctr === 5) {
resolveMethod(); // Work done, resolve the promise
} else {
await myRecursiveLogic(resolveMethod, ctr); // recursion - continue work
}
}
// Run the method with a single promise
new Promise((res) => myRecursiveLogic(res)).then(r => console.log('done'));
Here's a clean and nice solution using the latest NodeJS features.
The recursive function will continue executing until a specific condition is met (in this example asynchronously getting some data).
const sleep = require('util').promisify(setTimeout)
const recursive = async () => {
await sleep(1000)
const data = await getDataViaPromise() // you can replace this with request-promise
if (!data) {
return recursive() // call the function again
}
return data // job done, return the data
}
The recursive function can be used as follows:
const main = async () => {
const data = await recursive()
// do something here with the data
}
Using your code, I'd refactored it as shown below. I hope it helps.
const requestP = require('request-promise');
const option = {
url: 'rest/api/2/search',
json: true,
qs: {
//jql: "project in (FLAGPS)",
}
};
/*
NOTE: Add async to the function so you can udse await inside the function
*/
const callback = async (body) => {
// some code
//saving records to file
//some code
try {
const result = await requestP(option, callback).auth('api-reader', token, true);
if (totlExtractedRecords < total) {
return callback(result);
}
return result;
} catch (error) {
console.log('Error Observed ' + err);
return error;
}
}
Created this code using feed back from Amir Popovich
const rp = require('Request-Promise')
const fs = require('fs')
const pageSize = 200
const options = {
url: 'https://jira.xyz.com/rest/api/2/search',
json: true,
qs: {
jql: "project in (PROJECT_XYZ)",
maxResults: pageSize,
startAt: 0,
fields: '*all'
},
auth: {
user: 'api-reader',
pass: '<token>',
sendImmediately: true
}
}
const updateCSV = (elment) => {
//fs.writeFileSync('issuedata.json', JSON.stringify(elment.body, undefined, 4))
}
async function getPageinatedData(resolve, reject, ctr = 0) {
var total = 0
await rp(options).then((body) => {
let a = body.issues
console.log(a)
a.forEach(element => {
console.log(element)
//updateCSV(element)
});
total = body.total
}).catch((error) => {
reject(error)
return
})
ctr = ctr + pageSize
options.qs.startAt = ctr
if (ctr >= total) {
resolve();
} else {
await getPageinatedData(resolve, reject, ctr);
}
}
new Promise((resolve, reject) => getPageinatedData(resolve, reject))
.then(() => console.log('DONE'))
.catch((error) => console.log('Error observed - ' + error.name + '\n' + 'Error Code - ' + error.statusCode));

Categories

Resources