The code below shows how to validate a Mongo.Cursor. My Question is, why would someone want to validate a Mongo.cursor. It's a bit abstract for me. I mean if I manipulate data and click on a button, I understand the reason for validation, but on a Mongo.Cursor?
Template.Lists_show.onCreated(function listShowOnCreated() {
this.autorun(() => {
new SimpleSchema({
list: { type: Function },
todosReady: { type: Boolean },
todos: { type: Mongo.Cursor },
}).validate(Template.currentData());
});
Related
EDIT
added my solution as an answer
ORIGINAL QUESTION
i believe this issue has to do with circular dependencies. i spent the better half of last night and today trying everything i could find online but nothing seems to work.
what i have tried:
convert the fields prop to a function that returns a field object
convert the relating fields (within the fields prop) into functions that return the type
combining the two approaches above
finally ending with require statements in place of the fields that use the reference type (does not seem correct and the linter had a stroke over this one)
here is the file structure:
here is the code:
userType.js
const graphql = require('graphql');
const Connection = require('../../db/connection');
const ConnectionType = require('../connection/connectionType');
const { GraphQLObjectType, GraphQLList, GraphQLString, GraphQLID } = graphql;
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: GraphQLID },
username: { type: GraphQLString },
email: { type: GraphQLString },
created: {
type: GraphQLList(ConnectionType),
resolve: ({ id }) => Connection.find({ owner: id }),
},
joined: {
type: GraphQLList(ConnectionType),
resolve: ({ id }) => Connection.find({ partner: id }),
},
}),
});
module.exports = UserType;
connectionType.js
const graphql = require('graphql');
const User = require('../../db/user');
const UserType = require('../user/userType');
const { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLInt } = graphql;
const ConnectionType = new GraphQLObjectType({
name: 'Connection',
fields: () => ({
id: { type: GraphQLID },
owner: {
type: UserType,
resolve: ({ owner }) => User.findById(owner),
},
partner: {
type: UserType,
resolve: ({ partner }) => User.findById(partner),
},
title: { type: GraphQLString },
description: { type: GraphQLString },
timestamp: { type: GraphQLString },
lifespan: { type: GraphQLInt },
}),
});
module.exports = ConnectionType;
i couldnt get any help on this anywhere. in case anyone runs into this error message here are the steps i took to fix it:
switched from graphql-express to apollo-server-express (this was not necessary but i found apollo to be a more robust library)
used the following packages: graphql graphql-import graphql-tools
switched from javascript based Type defs to using the GraphQL SDL (.graphql) file type
step 3 is what corrected the circular import issue associated with one-to-many (and m2m) relationships
i committed every step of the refactor from dumping the old code to creating the new. i added plenty of notes and explicit naming so that it should be usable as a guide.
you can see the commit history diffs through the links below. all of the work until the last few commits was done within the graphql/ directory. if you click the title of the commit it will show you the diff so you can follow the refactor
Last refactor with one-to-many relationship using apollo and GraphQL SDL Type defs
commit history, start at Scrapped old GraphQL setup
after the refactor i now have cleaner resolvers, a better directory pattern, and, most importantly, fully functioning one-to-many relationships between User and Connection! ...only took my entire goddamn day.
the relationship in this case is:
Connection belongs to an owner (User through owner_id) and partner (User through partner_id).
we will be moving forward from here with the codebase but i locked the branch and its commits for anyone who needs a guide.
I had a similar issue using Typescript, and I kinda like the javascript based Type definition better so didn't change to GraphQL SDL.
I got it to work just by specifying the type of const to GraphQLObjectType.
Something like:
export const UserType: GraphQLObjectType = new GraphQLObjectType({
name: 'UserType',
fields: () => ({
.....
})
}
Now it works without a problem.
Every tutorial I have found thus far has achieved pagination in GraphQL via Apollo, Relay, or some other magic framework. I was hoping to find answers in similar asked questions here but they don't exist. I understand how to setup the queries but I'm unclear as to how I would implement the resolvers.
Could someone point me in the right direction? I am using mongoose/MongoDB and ES5, if that helps.
EDIT: It's worth noting that the official site for learning GraphQL doesn't have an entry on pagination if you choose to use graphql.js.
EDIT 2: I love that there are some people who vote to close questions before doing their research whereas others use their knowledge to help others. You can't stop progress, no matter how hard you try. (:
Pagination in vanilla GraphQL
// Pagination argument type to represent offset and limit arguments
const PaginationArgType = new GraphQLInputObjectType({
name: 'PaginationArg',
fields: {
offset: {
type: GraphQLInt,
description: "Skip n rows."
},
first: {
type: GraphQLInt,
description: "First n rows after the offset."
},
}
})
// Function to generate paginated list type for a GraphQLObjectType (for representing paginated response)
// Accepts a GraphQLObjectType as an argument and gives a paginated list type to represent paginated response.
const PaginatedListType = (ItemType) => new GraphQLObjectType({
name: 'Paginated' + ItemType, // So that a new type name is generated for each item type, when we want paginated types for different types (eg. for Person, Book, etc.). Otherwise, GraphQL would complain saying that duplicate type is created when there are multiple paginated types.
fields: {
count: { type: GraphQLInt },
items: { type: new GraphQLList(ItemType) }
}
})
// Type for representing a single item. eg. Person
const PersonType = new GraphQLObjectType({
name: 'Person',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: GraphQLString },
}
})
// Query type which accepts pagination arguments with resolve function
const PersonQueryTypes = {
people: {
type: PaginatedListType(PersonType),
args: {
pagination: {
type: PaginationArgType,
defaultValue: { offset: 0, first: 10 }
},
},
resolve: (_, args) => {
const { offset, first } = args.pagination
// Call MongoDB/Mongoose functions to fetch data and count from database here.
return {
items: People.find().skip(offset).limit(first).exec()
count: People.count()
}
},
}
}
// Root query type
const QueryType = new GraphQLObjectType({
name: 'QueryType',
fields: {
...PersonQueryTypes,
},
});
// GraphQL Schema
const Schema = new GraphQLSchema({
query: QueryType
});
and when querying:
{
people(pagination: {offset: 0, first: 10}) {
items {
id
name
}
count
}
}
Have created a launchpad here.
There's a number of ways you could implement pagination, but here's two simple example resolvers that use Mongoose to get you started:
Simple pagination using limit and skip:
(obj, { pageSize = 10, page = 0 }) => {
return Foo.find()
.skip(page*pageSize)
.limit(pageSize)
.exec()
}
Using _id as a cursor:
(obj, { pageSize = 10, cursor }) => {
const params = cursor ? {'_id': {'$gt': cursor}} : undefined
return Foo.find(params).limit(pageSize).exec()
}
I am trying to create CRUD app in sails js, and i am able to post data to my DB what i noticed is when i insert data on success sails return whole object. But if we don't want certain fields in response then how can we restrict it. Please help thanks.
module.exports = {
attributes : {
username : {
type: 'string',
required: true
},
password : {
type: 'string',
required: true
},
email : {
type: 'string',
required: true,
unique: true
}
},
toJson: function() {
var obj = this.toObject();
delete obj.password;
return obj;
},
beforeCreate: function(attribute, callback) {
console.log(attribute.password);
require('bcrypt').hash(attribute.password, 10, function(err, encryptedPassword) {
sails.log(err);
attribute.password = encryptedPassword;
sails.log(encryptedPassword);
callback();
});
}
};
#arbuthnott is partly correct above -- you do need toJSON rather than toJson -- but more importantly, the function needs to go inside the attributes dictionary, since it is an instance method:
attributes : {
username : {
type: 'string',
required: true
},
password : {
type: 'string',
required: true
},
email : {
type: 'string',
required: true,
unique: true
},
toJSON: function() {
var obj = this.toObject();
delete obj.password;
return obj;
}
}
I think the responses through sails default REST api for models runs them through .toJSON before returning, so you are doing this the right way.
However, you may have a case issue, like you should define .toJSON with uppercase instead of .toJson. Try making that switch and see if it solves your problem.
UPDATE
Sounds like this is not solving your issue. The sails docs from here say:
The real power of toJSON relies on the fact every model instance sent out via res.json is first passed through toJSON. Instead of writing custom code for every controller action that uses a particular model (including the "out of the box" blueprints), you can manipulate outgoing records by simply overriding the default toJSON function in your model. You would use this to keep private data like email addresses and passwords from being sent back to every client.
That sounds pretty explicitly like what we are trying to do, so maybe this is a sails bug. Perhaps it applies to find, but not create. Is that password returned when simply finding an existing user?
If you must, a sure way around this would be to override the default create action in your UserController:
create: function(req, res) {
User.create(req.body).exec(function(err, user) {
if (err) {
return res.json(err);
}
// explicitly call your own toJSON() to be sure
return res.send(user.toJSON());
});
},
This isn't ideal, especially if you have many model properties you want to hide in many api calls. But it will get the job done.
password: { type: 'string', required: true, protected: true }
protected:true is now deprecated on sails v1.0
You can use instead of that customToJSON
customToJSON: function() {
// Return a shallow copy of this record with the password and ssn removed.
return _.omit(this, ['password', 'ssn'])
}
password: { type: 'string', required: true, protected: true }
You can do this also.
In my Stacks schema i have a dimensions property defined as such:
dimensions: {
type: [String],
autoform: {
options: function() {
return Dimensions.find().map(function(d) {
return { label: d.name, value: d._id };
});
}
}
}
This works really well, and using Mongol I'm able to see that an attempt to insert data through the form worked well (in this case I chose two dimensions to insert)
However what I really what is data that stores the actual dimension object rather than it's key. Something like this:
[
To try to achieve this I changed type:[String] to type:[DimensionSchema] and value: d._id to value: d. The thinking here that I'm telling the form that I am expecting an object and am now returning the object itself.
However when I run this I get the following error in my console.
Meteor does not currently support objects other than ObjectID as ids
Poking around a little bit and changing type:[DimensionSchema] to type: DimensionSchema I see some new errors in the console (presumably they get buried when the type is an array
So it appears that autoform is trying to take the value I want stored in the database and trying to use that as an id. Any thoughts on the best way to do this?.
For reference here is my DimensionSchema
export const DimensionSchema = new SimpleSchema({
name: {
type: String,
label: "Name"
},
value: {
type: Number,
decimal: true,
label: "Value",
min: 0
},
tol: {
type: Number,
decimal: true,
label: "Tolerance"
},
author: {
type: String,
label: "Author",
autoValue: function() {
return this.userId
},
autoform: {
type: "hidden"
}
},
createdAt: {
type: Date,
label: "Created At",
autoValue: function() {
return new Date()
},
autoform: {
type: "hidden"
}
}
})
According to my experience and aldeed himself in this issue, autoform is not very friendly to fields that are arrays of objects.
I would generally advise against embedding this data in such a way. It makes the data more difficult to maintain in case a dimension document is modified in the future.
alternatives
You can use a package like publish-composite to create a reactive-join in a publication, while only embedding the _ids in the stack documents.
You can use something like the PeerDB package to do the de-normalization for you, which will also update nested documents for you. Take into account that it comes with a learning curve.
Manually code the specific forms that cannot be easily created with AutoForm. This gives you maximum control and sometimes it is easier than all of the tinkering.
if you insist on using AutoForm
While it may be possible to create a custom input type (via AutoForm.addInputType()), I would not recommend it. It would require you to create a template and modify the data in its valueOut method and it would not be very easy to generate edit forms.
Since this is a specific use case, I believe that the best approach is to use a slightly modified schema and handle the data in a Meteor method.
Define a schema with an array of strings:
export const StacksSchemaSubset = new SimpleSchema({
desc: {
type: String
},
...
dimensions: {
type: [String],
autoform: {
options: function() {
return Dimensions.find().map(function(d) {
return { label: d.name, value: d._id };
});
}
}
}
});
Then, render a quickForm, specifying a schema and a method:
<template name="StacksForm">
{{> quickForm
schema=reducedSchema
id="createStack"
type="method"
meteormethod="createStack"
omitFields="createdAt"
}}
</template>
And define the appropriate helper to deliver the schema:
Template.StacksForm.helpers({
reducedSchema() {
return StacksSchemaSubset;
}
});
And on the server, define the method and mutate the data before inserting.
Meteor.methods({
createStack(data) {
// validate data
const dims = Dimensions.find({_id: {$in: data.dimensions}}).fetch(); // specify fields if needed
data.dimensions = dims;
Stacks.insert(data);
}
});
The only thing i can advise at this moment (if the values doesnt support object type), is to convert object into string(i.e. serialized string) and set that as the value for "dimensions" key (instead of object) and save that into DB.
And while getting back from db, just unserialize that value (string) into object again.
I'm using aldeed:simple-schema and here's the code:
Cities = new Mongo.Collection('cities');
Cities.insert({
name: 'Oslo'
});
Cities.insert({
name: 'Helsinki'
});
Contact = new SimpleSchema({
city: {
type: String,
allowedValues: Cities.find().map((e) => e.name) // written ES6-style for readability; in fact, here goes an ES5 anonymous function definition
}
});
What it does is explicitly binds currently existing cities from Cities collection to Contact schema's certain field's allowed values, so it's then impossible to store any other value than "Oslo" or "Helsinki".
But when posting a quickForm, the field (select, actually) has no options.
If I rewrite the mapping function to
(e) => {
console.log(e);
return e.name;
}
then I get
I20150911-18:07:23.334(4)? { _id: 'GLAbPa6N4W4c9GZZh', name: 'Oslo' }
I20150911-18:07:23.333(4)? { _id: 'vb64X5mKpMbDNzCkw', name: 'Helsinki' }
in server logs, which makes me think the mapping function is correct.
At the very same time, doing all this in Mongo console returns desirable result:
production-d:PRIMARY> db.cities.find().map(function (e) { return e.name; });
[ "Oslo", "Helsinki" ]
What do I do wrong? Is it impossible to fill the simple-schema's allowedValues array at the run time?