Comparing results from two API calls and returning their difference in MEAN app - angularjs

EDIT: Since I wasn't able to find a correct solution, I changed the
application's structure a bit and posted another question:
Mongoose - find documents not in a list
I have a MEAN app with three models: User, Task, and for keeping track of which task is assigned to which user I have UserTask, which looks like this:
const mongoose = require("mongoose");
const autopopulate = require("mongoose-autopopulate");
const UserTaskSchema = mongoose.Schema({
completed: { type: Boolean, default: false },
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
autopopulate: true
},
taskId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Task",
autopopulate: true
}
});
UserTaskSchema.plugin(autopopulate);
module.exports = mongoose.model("UserTask", UserTaskSchema);
In my frontend app I have AngularJS services and I already have functions for getting all users, all tasks, and tasks which are assigned to a particular user (by getting all UserTasks with given userId. For example:
// user-task.service.js
function getAllUserTasksForUser(userId) {
return $http
.get("http://localhost:3333/userTasks/byUserId/" + userId)
.then(function(response) {
return response.data;
});
}
// task-service.js
function getAllTasks() {
return $http.get("http://localhost:3333/tasks").then(function(response) {
return response.data;
});
}
Then I'm using this data in my controllers like this:
userTaskService
.getAllUserTasksForUser($routeParams.id)
.then(data => (vm.userTasks = data));
...and because of autopopulate plugin I have complete User and Task objects inside the UserTasks that I get. So far, so good.
Now I need to get all Tasks which are not assigned to a particular User. I guess I should first get all Tasks, then all UserTasks for a given userId, and then make some kind of difference, with some "where-not-in" kind of filter.
I'm still a newbie for all the MEAN components, I'm not familiar with all those then()s and promises and stuff... and I'm really not sure how to do this. I tried using multiple then()s but with no success. Can anyone give me a hint?

You can do at server/API side that will more efficient.
In client side, if you want to do then try below
var userid = $routeParams.id;
userTaskService
.getAllTasks()
.then((data) => {
vm.userTasks = data.filter(task => task.userId !== userid)
});

Related

How can I map data of multiple collections in snapshot?

I am not too confident working with Firestore and have trouble with more complex API calls to get data. Usually I use SQL backends in my apps.
For the section that I am working on, I would like to combine three collections to get an array of ToDos with the involved users and the category the current user labelled this ToDo with. Every involved person can label the ToDo like they prefer, which makes things a little more complicated. Broken down the collections are structured as follows.
todo: Firestore Database Document
{
title: string,
involved: string[], //user ids
involvedCategory: string[] //category ids mapped by index to involved
}
(I tried to have an array of objects here instead of the two arrays, but it seems I would not be able to query the array for the current user´s ID, like mentioned here, so this is a workaround)
category: Firestore Database Document
{
title: string,
color: string
}
user: Firebase Authentication User
{
uid: string,
displayName: string,
photoURL: string,
...
}
THE GOAL
An array of ToDo items like this:
{
id: string,
title: string,
involved: User[],
category?: {
title: string,
color: string
}
}
As I am working with TypeScript, I created an interface to use a converter with. My code looks like this so far:
import {
DocumentData,
FirestoreDataConverter,
WithFieldValue,
QueryDocumentSnapshot,
SnapshotOptions,
query,
collection,
where,
} from 'firebase/firestore'
import { store } from '../firebase'
import { useCollectionData } from 'react-firebase-hooks/firestore'
import { User } from 'firebase/auth'
import { useCategories } from './categories'
import { useAuth } from '../contexts/AuthContext'
interface ToDo {
id: string
title: string
involved: User[]
category?: {
title: string
color: string
}
}
const converter: FirestoreDataConverter<ToDo> = {
toFirestore(todo: WithFieldValue<ToDo>): DocumentData {
return {} //not implemented yet
},
fromFirestore(
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
): ToDo {
const data = snapshot.data(options)
return {
id: snapshot.id,
title: data.title,
category: undefined, //?
involved: [], //?
}
},
}
export function useToDos() {
const { currentUser } = useAuth()
const { categories } = useCategories() //needed in converter
const ref = query(
collection(store, 'habits'),
where('involved', 'array-contains', currentUser.uid)
).withConverter(converter)
const [data] = useCollectionData(ref)
return {
todos: data,
}
}
Is there any way I can do this? I have a Hook that returns all of the user´s categories, but I obviously can´t call that outside the
useToDos-Hook. And creating the const in the hook does not help, either, as it results in an infinite re-render.
I know this is a long one, but does anyone have tips how I could approach this? Thanks in advance ^^
UPDATE:
I had to make two small adjustments to #ErnestoC ´s solution in case anyone is doing something similar:
First, I changed the calls for currentUser.id to currentUser.uid.
Afterwards I got the very missleading Firestore Error: PERMISSION_DENIED: Missing or insufficient permissions, which made me experiment a lot with my security rules. But that is not where the error originated. Debugging the code line by line, I noticed the category objects resolved by the promise where not correct and had a weird path with multiple spaces at the beginning and the end of their ids. When I removed them before saving them in the promises array, it worked. Although I do not see where the spaces came from in the first place.
promises.push(
getDoc(
doc(
store,
'categories',
docSnap.data().involvedCategory[userCatIndex].replaceAll(' ', '')
)
)
)
The general approach, given that Firestore is a NoSQL database that does not support server-side JOINS, is to perform all the data combinations on the client side or in the backend with a Cloud Function.
For your scenario, one approach is to first query the ToDo documents by the array membership of the current user's ID in the involved array.
Afterwards, you fetch the corresponding category document the current user assigned to that ToDo (going by index mapping between the two arrays). Finally, you should be able to construct your ToDo objects with the data.
const toDoArray = [];
const promises = [];
//Querying the ToDo collection
const q = query(collection(firestoreDB, 'habits'), where('involved', 'array-contains', currentUser.id));
const querySnap = await getDocs(q);
querySnap.forEach((docSnap) => {
//Uses index mapping
const userCatIndex = docSnap.data().involved.indexOf(currentUser.id);
//For each matching ToDo, get the corresponding category from the categories collection
promises.push(getDoc(doc(firestoreDB, 'categories', docSnap.data().involvedCategory[userCatIndex])));
//Pushes object to ToDo class/interface
toDoArray.push(new ToDo(docSnap.id, docSnap.data().title, docSnap.data().involved))
});
//Resolves all promises of category documents, then adds the data to the existing ToDo objects.
await Promise.all(promises).then(categoryDocs => {
categoryDocs.forEach((userCategory, i) => {
toDoArray[i].category = userCategory.data();
});
});
console.log(toDoArray);
Using the FirestoreDataConverter interface would not be that different, as you would need to still perform an additional query for the category data, and then add the data to your custom objects. Let me know if this was helpful.

Stripe webhook checkout.session items to Supabase

Im using next.js and Stripe webhooks to insert checkout sessions to Supabase that will create a customer's order history. I'm able to get the information about the whole order written to a table called 'orders', but am wondering what the best way to add individual items within each checkout session to another table called 'order_items' is. This way I can map through main orders and then the children items. Appreciate any help provided. Here is what I have for getting orders associated with a customer:
const upsertOrderRecord = async (session: Stripe.Checkout.Session, customerId: string) => {
const { data: customerData, error: noCustomerError } = await supabaseAdmin
.from<Customer>('customers')
.select('id')
.eq('stripe_customer_id', customerId)
.single();
if (noCustomerError) throw noCustomerError;
const { id: uuid } = customerData || {};
const sessionData: Session = {
id: session.id,
amount_total: session.amount_total ?? undefined,
user_id: uuid ?? undefined
};
const { error } = await supabaseAdmin.from<Session>('orders').insert([sessionData], { upsert: true });
if (error) throw error;
console.log(`Product inserted/updated: ${session.id}`);
};
The Checkout Session object contains a line_items field which is a list of each item included in the purchase.
However this field is not included in the object by default, and therefore won't be a part of your webhook payload. Instead you'll need to make an API call in your webhook handle to retrieve the Checkout Session object, passing the expand parameter to include the line_items field:
const session = await stripe.checkout.sessions.retrieve('cs_test_xxx', {
expand: ['line_items']
});

configParams undefined in getData() call

I'm in the process of building a community connector and am scratching my head; the documentation states:
getData()
Returns the tabular data for the given request.
Request
#param {Object} request A JavaScript object containing the data
request parameters.
The request parameter contains user provided values and additional
information that can be used to complete the data request. It has the
following structure:
{ "configParams": object, "scriptParams": {
"sampleExtraction": boolean,
"lastRefresh": string }, "dateRange": {
"startDate": string,
"endDate": string }, "fields": [
{
object(Field)
} ] }
I've correctly setup getConfig() (at least, my configurations are requested from the user), but my getData function is not being passed a configParams object. Here's my code.
function getConfig(request) {
var Harvest = HarvestService({
token: getHarvestAuthService().getAccessToken()
});
var accounts = Harvest.accounts.list();
var options = accounts.map(function(account) {
return {
label: account.name,
value: account.id
};
});
var config = {
configParams: [
{
type: 'SELECT_SINGLE',
name: 'harvestAccountId',
displayName: 'Harvest Account ID',
helpText: 'The ID of the Harvest Account to pull data from.',
options: options
}
],
dateRangeRequired: true
};
return config;
}
function getData(request) {
var startDate = request.dateRange.startDate;
var endDate = request.dateRange.endDate;
var accountId = request.configParams.harvestAccountId;
var harvestAuthService = getHarvestAuthService();
var Harvest = HarvestService({
token: harvestAuthService.getAccessToken(),
account: accountId
});
var fieldKeys = request.fields.map(function(field) { return field.name; });
var entries = Harvest.entries.list({
startDate: new Date(startDate),
endDate: new Date(endDate)
});
var rows = entries.map(entryToRow);
return {
schema: request.fields,
rows: rows,
cachedData: false
};
}
When I test/debug, I can select an Account at the config step, the schema is correctly returned, but I get the following exception when I try and add a widget to the report:
Script error message:
TypeError: Cannot read property "harvestAccountId" from undefined.
Script error cause: USER Script
error stacktrace: getData:244
Any advice greatly appreciated.
Found out the problem - the issue was that the value attribute of the option was a number, but it MUST be a string:
https://developers.google.com/datastudio/connector/reference#getconfig
Leaving this here in case anyone else gets stuck on this. Your config select options for your Data Studio Community Connector must have strings for both the label and the value, and nobody will coerce them for you. Fix was this:
var options = accounts.map(function(account) {
return {
label: account.name,
value: account.id + ''
};
});
Usually, request.configParams is undefined when there are no configuration values passed from the user config.
When testing the connector, are you selecting a value in the dropdown for harvestAccountId?
If you plan to share this connector with other users, it might be a good idea to have a default value for harvestAccountId in case the user does not select an option.
You can use Apps Script logging to see the response for getConfig() to ensure that right values are getting passed for options. Then you can also log the request for getData() to have a better understanding of what exactly is getting passed in the request.
Leaving this in case it helps anyone, note that the config params in the UI although they have a placeholder value need to be physically populated to appear in the request. Indeed, if none of these are filled in the configParams value does not appear in the request object.

Meteor Publish & Subscribe Not returning results using selector

I have the following code:
import { Meteor } from 'meteor/meteor';
import { Items } from './collection';
if (Meteor.isServer) {
Meteor.publish('items', function(options, owner) {
let selector = {
$and: [{ ownerId: owner}]
}
return Items.find(selector, options);
});
}
And on the client side I have:
this.subscribe('items', () => [{
limit: this.getReactively('querylimit'),
sort: {dateTime: -1}
},
this.getReactively('ownerId')
]);
The above does not return any results. However, when I change the return statement to the following, it works!
return Items.find({ ownerId: '7QcWm55wGw69hpuy2' }, options); //works !!!
I'm not very familiar with Mongo/Meteor query selectors. Passing the query as a variable to Items.find() seems to be messing something up. Can someone please help me figure this out!
Thanks
You are trying to pass a function as the selector, which won't work. Functions can't be serialized and sent from the client to the server. Instead you need to evaluate the options and the owner separately. Here's an example:
var owner = this.getReactively('ownerId');
var options = {
limit: this.getReactively('querylimit'),
sort: {dateTime: -1}
};
this.subscribe('items', options, owner);
Note that the published documents will not arrive in sorted order, so unless you are using a limit, the sort doesn't help here.
Also note that if you need the subscription to rerun after the owner or query limit change, you'll need to subscribe inside of an autorun.
Here's a start on an improved implementation:
Meteor.publish('items', function(options, owner) {
// DANGER! Actually check this against something safe!
check(options, Object);
// DANGER! Should any user subscribe for any owner's items?
check(owner, Match.Maybe(String));
// Publish the current user's items by default.
if (!owner) {
owner = this.userId;
}
return Items.find({ ownerId: owner }, options);
});

Posting Schema.Types.ObjectId arrays to MongoDB

How can I post an array of Schema.Types.ObjectId (s) to MongoDB? I'm trying to create User Groups, which is a group of the 'User' Model e.g.
var UserGroup = new Schema({
users: [{
type: Schema.Types.ObjectId,
ref: 'User'
}]
});
New UserGroup Function
module.exports.create = function(request, response) {
var group = new UserGroup({
users = request.body.users
});
group.save(function(error) {
if(error) { throw error; } else { response.send('Group Created Successfully.');
});
};
I'm currently using Postman to test the functionality, how exactly should the data be posted?
As a Javascript array i.e ['A_USER_ID', 'A_USER_ID'] ?
Thanks!
#Answer
I was using the older syntax of the select() function, and therefore was passing invalid parameters to the $push function. When sending the request, I simply pass the ObjectIds as id,id,id and once they get to the server, simply put it into an array using var my_array = request.body.users.split(','); and then push it to the database using the following:
$push: { users: { $each: my_array } }
I hope this was helpful, the documentation isn't particularly clear on this matter.

Resources