Mongodb, query within key-pair values - javascript

Is there any way to query inside this struture by the object keys of roles?
interface UsersModel{
_id: string;
email: string;
roles: { [key: string]: RolesType[] };
}
For now I get the data into users like:
roles: {
$ne: null
}
and then
const finalResult = users.filter(u => Object.keys(u.roles).includes('objectKeyIwant'));
Can this be done on the mongodb side and avoid getting all the data then filter it?
Data examples:
The user collection is like this:
{
_id: '610baff85c071a2bd59dc84f',
email: 'some#email.com',
roles : {
"shop1" : [
"shop_stock_manager"
],
"shop2" : [
"shop_admin",
"shop_stock_manager"
]
}
}
And I want to get all the users that have any role in shop2.

use this
db.collection.find({"roles.shop2":{$ne:[]}})

Related

Array of {key: value} in mongoose schema

I have a comments field in my interface and schema as follows
export interface Blog extends Document {
...
comments: { user: ObjectId; comment: string }[]
...
}
const blogSchema = new Schema<Blog>({
...
comments: [
{
user: {
type: ObjectId
},
comment: {
type: String
}
}
],
...
})
However, schema throws this error 'ObjectId' only refers to a type but is being used as a value here.. I know that schema is written kind of in a weird way, but I'm confused with how to improve.
What I want in database is something like
[
{
user: "Vivid",
comment: "Vivid's comment"
},
{
user: "David",
comment: "David's comment"
}
]
I believe you need to change ObjectId to
mongoose.Schema.Types.ObjectId
Either that, or you could do something like:
const mongoose = require('mongoose');
// ... other imports
const { ObjectId } = mongoose.Schema.Types;
// ... now use in code as `ObjectId`

MongoDB Aggregate is not matching specific field

I'm new to Aggregation in MongoDB and I'm trying to understand the concepts of it by making examples.
I'm trying to paginate my subdocuments using aggregation but the returned document is always the overall values of all document's specific field.
I want to paginate my following field which contains an array of Object IDs.
I have this User Schema:
const UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
firstname: String,
lastname: String,
following: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
...
}, { timestamps: true, toJSON: { virtuals: true }, toObject: { getters: true, virtuals: true } });
Without aggregation, I am able to paginate following,
I have this route which gets the user's post by their username
router.get(
'/v1/:username/following',
isAuthenticated,
async (req, res, next) => {
try {
const { username } = req.params;
const { offset: off } = req.query;
let offset = 0;
if (typeof off !== undefined && !isNaN(off)) offset = parseInt(off);
const limit = 2;
const skip = offset * limit;
const user = await User
.findOne({ username })
.populate({
path: 'following',
select: 'profilePicture username fullname',
options: {
skip,
limit,
}
})
res.status(200).send(user.following);
} catch (e) {
console.log(e);
res.status(500).send(e)
}
}
);
And my pagination version using aggregate:
const following = await User.aggregate([
{
$match: { username }
},
{
$lookup: {
'from': User.collection.name,
'let': { 'following': '$following' },
'pipeline': [
{
$project: {
'fullname': 1,
'username': 1,
'profilePicture': 1
}
}
],
'as': 'following'
},
}, {
$project: {
'_id': 0,
'following': {
$slice: ['$following', skip, limit]
}
}
}
]);
Suppose I have this documents:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'gagi',
following: []
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'kuku',
following: []
},
{
_id: '76jghkdfhasjhfsdkf',
username: 'john',
following: ['5fdgffdgfdgdsfsdfsf', '5fgjhkljvlkdsjfsldkf']
},
]
And when I test my route for user john: /john/following, everything is fine but when I test for different user which doesn't have any following: /gagi/following, the returned result is the same as john's following which aggregate doesn't seem to match user by username.
/john/following | following: 2
/kuku/following | following: 0
Aggregate result:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'kuku',
...
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'gagi',
...
}
]
I expect /kuku/following to return an empty array [] but the result is same as john's. Actually, all username I test return the same result.
I'm thinking that there must be wrong with my implementation since I've only started exploring aggregation.
Mongoose uses a DBRef to be able to populate the field after it has been retrieved.
DBRefs are only handled on the client side, MongoDB aggregation does not have any operators for handling those.
The reason that aggregation pipeline is returning all of the users is the lookup's pipeline does not have a match stage, so all of the documents in the collection are selected and included in the lookup.
The sample document there is showing an array of strings instead of DBRefs, which wouldn't work with populate.
Essentially, you must decide whether you want to use aggregation or populate to handle the join.
For populate, use the ref as shown in that sample schema.
For aggregate, store an array of ObjectId so you can use lookup to link with the _id field.

How to insert default values to foreign object with relation using Postgresql, Knex.js and Objection.js with HasOneRelation?

I'm setting up simple API using Postgresql, Knex.js and Objection.js. I created User model with "location" property. This "location" property is another table. How I have to insert that user to database with defaults 'city' and 'country' in 'location' property?
I already tried to use 'static get jsonSchema' in model itself and 'allowInsert' method in mutation but when I fetching created that user the 'location' still 'null'.
So, let's say we have migration for users_table:
exports.up = knex =>
knex.schema.createTable('users', table => {
table.increments('id').primary();
table
.string('email')
.unique()
.notNullable();
table.string('firstName').notNullable();
table.string('lastName').notNullable();
table.string('password').notNullable();
});
exports.down = knex => knex.schema.dropTable('users');
And we have location_table:
exports.up = knex =>
knex.schema.createTable('locations', table => {
table.increments('id').primary();
table.string('country').defaultTo('USA');
table.string('city').defaultTo('San Francisco');
table
.integer('user_id')
.references('id')
.inTable('users')
.onUpdate('CASCADE')
.onDelete('CASCADE');
});
exports.down = knex => knex.schema.dropTable('locations');
Here User Model with objection.js:
export default class User extends Model {
static get tableName() {
return 'users';
}
// wrong probably
static get jsonSchema() {
return {
type: 'object',
properties: {
location: {
type: 'object',
properties: {
city: {
type: 'string',
default: 'Los Angeles',
},
country: {
type: 'string',
default: 'USA',
},
},
},
},
};
}
fullName() {
return `${this.firstName} ${this.lastName}`;
}
static get relationMappings() {
return {
location: {
relation: Model.HasOneRelation,
modelClass: Location,
join: {
from: 'users.id',
to: 'locations.user_id',
},
},
};
}
}
And Location model:
export default class Location extends Model {
static get tableName() {
return 'locations';
}
static get relationMappings() {
return {
user: {
relation: Model.BelongsToOneRelation,
modelClass: `${__dirname}/User`,
join: {
from: 'locations.user_id',
to: 'users.id',
},
},
};
}
}
My mutation when I creating new User:
// ...
const payload = {
email,
firstName,
lastName,
password: hash,
};
const newUser = await User.query()
.allowInsert('[user, location]')
.insertAndFetch(payload);
// ...
And in the end query:
// ...
User.query()
.eager('location')
.findOne({ email });
// ...
From query of user I expect to see the object with locatoin propety with my defaults. Example:
{
email: 'jacklondon#gmail.com',
firstName: 'Jack',
fullName: 'Jack London',
id: '1',
lastName: 'London',
location: {
city: 'San Francisco',
country: 'USA',
},
userName: 'jacklondon1',
__typename: 'User',
}
So, where I made mistake with such simple operation?
One to One Solution
I think part of the issue is that your allow insert included the user object. You shouldn't include the user in the allow insert because it's implicit since you're on the User model (example). The other issue you had was that you were trying to use insertAndFetch method. insertAndFetch cannot be used when inserting a graph. You need to use the insertGraph method to insert a graph (docs). Since you are using Postgres, you can chain the returning(*) method and it will return the result without additional queries (example). Finally, since you're asking for a one-to-one relation, you have to specify a city and country every time. Objection will not know it needs to create a new row without specifying it (even if you have configured the database to have default values). The way I accomplished this for you was to use default parameters.
const createNewuser = async (email, firstName, lastName, hash, city = 'Los Angeles', country = 'USA') => {
const newUser = await User
.query()
.insertGraph({
email,
firstName,
lastName,
password: hash,
location: {
city: city,
country: country
}
})
.returning('*');
return newUser;
}
Additional Thought to Ponder
I'm not sure why you have a one-to-one relationship between user and location. Why not just make city, state, and country part of the user's table since it's already one to one?
However, what I think you're really going for is a one-to-many relationship between user and location. One location has multiple users. This would put you into 3rd normal form by reducing the amount of duplicate data in your database since you wouldn't duplicate a city/country for each user in an identical location.
If you're just learning objection, I would recommend reading up on graphs in the documentation.

Undesirable structure when populating sub document with Mongoose

A user has project IDs but I also want to store some additional project info:
const userSchema = new Schema({
...
projects: [{
_id: {
type: Schema.Types.ObjectId,
ref: 'Project',
unique: true, // needed?
},
selectedLanguage: String,
}]
});
And I want to populate with the project name so I'm doing:
const user = await User
.findById(req.user.id, 'projects')
.populate('projects._id', 'name')
.exec();
However user.projects gives me this undesirable output:
[
{
selectedLanguage: 'en',
_id: { name: 'ProjectName', _id: 5a50ccde03c2d1f5a07e0ff3 }
}
]
What I wanted was:
[
{ name: 'ProjectName', _id: 5a50ccde03c2d1f5a07e0ff3, selectedLanguage: 'en' }
]
I can transform the data but I'm hoping that Mongoose can achieve this out the box as it seems a common scenario? Thanks.
Seems like there are two options here:
1) Name the _id field something more semantic so it's:
{
selectedLanguage: 'en',
somethingSemantic: { _id: x, name: 'ProjectName' },
}
2) Flatten the data which can be done generically with modern JS:
const user = await User
.findById(req.user.id, 'projects')
.populate('projects._id', 'name')
.lean() // Important to use .lean() or you get mongoose props spread in
.exec();
const projects = user.projects.map(({ _id, ...other }) => ({
..._id,
...other,
}));
try something like this
populate({path:'projects', select:'name selectedLanguage'})

GraphQL mutation that accepts an array of dynamic size and common scalar types in one request

I need to be able to create a user and add it's favourite movies (An array of objects with a reference to the Movies collection and his personal rating for each movie) in a single request.
Something that could look like this (pseudocode)
var exSchema = `
type Mutation {
addUser(
name: String!
favMovies: [{ movie: String! #ref to movies coll
personal_rating: Int! # this is different for every movie
}]
) : User
}
...
`
What is the graphql way of doing this in a single request? I know I can achieve the result with multiple mutations/requests but I would like to do it in a single one.
You can pass an array like this
var MovieSchema = `
type Movie {
name: String
}
input MovieInput {
name: String
}
mutation {
addMovies(movies: [MovieInput]): [Movie]
}
`
Then in your mutation, you can pass an array like
mutation {
addMovies(movies: [{name: 'name1'}, {name: 'name2'}]) {
name
}
}
Haven't tested the code but you get the idea
I came up with this simple solution - NO JSON used. Only one input is used. Hope it will help someone else.
I had to add to this type:
type Option {
id: ID!
status: String!
products: [Product!]!
}
We can add to mutation type and add input as follows:
type Mutation {
createOption(data: [createProductInput!]!): Option!
// other mutation definitions
}
input createProductInput {
id: ID!
name: String!
price: Float!
producer: ID!
status: String
}
Then following resolver could be used:
const resolvers = {
Mutation: {
createOption(parent, args, ctx, info) {
const status = args.data[0].status;
// Below code removes 'status' from all array items not to pollute DB.
// if you query for 'status' after adding option 'null' will be shown.
// But 'status': null should not be added to DB. See result of log below.
args.data.forEach((item) => {
delete item.status
});
console.log('args.data - ', args.data);
const option = {
id: uuidv4(),
status: status, // or if using babel status,
products: args.data
}
options.push(option)
return option
},
// other mutation resolvers
}
Now you can use this to add an option (STATUS is taken from first item in the array - it is nullable):
mutation{
createOption(data:
[{
id: "prodB",
name: "componentB",
price: 20,
producer: "e4",
status: "CANCELLED"
},
{
id: "prodD",
name: "componentD",
price: 15,
producer: "e5"
}
]
) {
id
status
products{
name
price
}
}
}
Produces:
{
"data": {
"createOption": {
"id": "d12ef60f-21a8-41f3-825d-5762630acdb4",
"status": "CANCELLED",
"products": [
{
"name": "componentB",
"price": 20,
},
{
"name": "componentD",
"price": 15,
}
]
}
}
}
No need to say that to get above result you need to add:
type Query {
products(query: String): [Product!]!
// others
}
type Product {
id: ID!
name: String!
price: Float!
producer: Company!
status: String
}
I know it is not the best way, but I did not find a way of doing it in documentation.
I ended up manually parsing the correct schema, since JavaScript Arrays and JSON.stringify strings were not accepted as graphQL schema format.
const id = 5;
const title = 'Title test';
let formattedAttachments = '';
attachments.map(attachment => {
formattedAttachments += `{ id: ${attachment.id}, short_id: "${attachment.shortid}" }`;
// { id: 1, short_id: "abcxyz" }{ id: 2, short_id: "bcdqrs" }
});
// Query
const query = `
mutation {
addChallengeReply(
challengeId: ${id},
title: "${title}",
attachments: [${formattedAttachments}]
) {
id
title
description
}
}
`;
What i understand by your requirement is that if you have the following code
const user = {
name:"Rohit",
age:27,
marks: [10,15],
subjects:[
{name:"maths"},
{name:"science"}
]
};
const query = `mutation {
createUser(user:${user}) {
name
}
}`
you must be getting something like
"mutation {
createUser(user:[object Object]) {
name
}
}"
instead of the expected
"mutation {
createUser(user:{
name: "Rohit" ,
age: 27 ,
marks: [10 ,15 ] ,
subjects: [
{name: "maths" } ,
{name: "science" }
]
}) {
name
}
}"
If this is what you wanted to achieve, then gqlast is a nice tag function which you can use to get the expected result
Simply grab the js file from here and use it as:
const user = {
name:"Rohit",
age:27,
marks: [10,15],
subjects:[
{name:"maths"},
{name:"science"}
]
};
const query = gqlast`mutation {
createUser(user:${user}) {
name
}
}`
The result stored in the variable query will be :
"mutation {
createUser(user:{
name: "Rohit" ,
age: 27 ,
marks: [10 ,15 ] ,
subjects: [
{name: "maths" } ,
{name: "science" }
]
}) {
name
}
}"
Pass them as JSON strings. That's what I do.
For those of you who don't need to pass in an array for one request, and are open to the idea of making a request for every mutation. (I am using Vue3, compisition Api, but React and Angular developers still can understand this).
You cannot for loop the mutation like this:
function createProject() {
for (let i = 0; i < state.arrOfItems.length; i++) {
const { mutate: addImplementation } = useMutation(
post_dataToServer,
() => ({
variables: {
implementation_type_id: state.arrOfItems[i],
sow_id: state.newSowId,
},
})
);
addImplementation();
}
}
this will give you an error, because the mutation must be in the setup().
(here is the error you will recieve: https://github.com/vuejs/vue-apollo/issues/888)
Instead create a child component, and map the array in the parent.
in Parent.vue
<div v-for="(card, id) in state.arrOfItems">
<ChildComponent
:id="id"
:card="card"
/>
</div>
in ChildComponent.vue
recieve props and:
const { mutate: addImplementation } = useMutation(
post_dataToServer,
() => ({
variables: {
implementation_id: props.arrOfItems,
id: props.id,
},
})
);

Categories

Resources